Skip to main content
Nexus Design System
GitHub

Switch

Switch turns a setting on or off right away, with no save step. Pair it with a Label so the label text is clickable too. For a choice that waits for a Save button, use Checkbox.

'use client';

import * as React from 'react';

import { Label } from '@/components/nxds/components/label/label';
import { Switch } from '@/components/nxds/components/switch/switch';

export default function SwitchWithLabel() {
  const switchId = React.useId();

  return (
    <div className="nx:flex nx:items-center nx:gap-2">
      <Switch id={switchId} />
      <Label htmlFor={switchId}>Airplane mode</Label>
    </div>
  );
}

This example also needs: Label.

Installation

npx nxds-cli@latest add switch

Run npx nxds-cli@latest init first if nxds isn’t set up in your app yet. See Install.

Usage

import { Switch } from '@/components/nxds/components/switch/switch';

Examples

Checked

Set defaultChecked to start the switch in the on position.

'use client';

import { Switch } from '@/components/nxds/components/switch/switch';

export default function SwitchChecked() {
  return <Switch aria-label="Toggle switch" defaultChecked />;
}

Disabled

A disabled switch shows muted colors and ignores clicks.

'use client';

import { Switch } from '@/components/nxds/components/switch/switch';

export default function SwitchDisabled() {
  return <Switch aria-label="Toggle switch" disabled />;
}

Small

Set size="sm" for dense rows.

'use client';

import { Switch } from '@/components/nxds/components/switch/switch';

export default function SwitchSmall() {
  return <Switch size="sm" aria-label="Toggle switch" />;
}

Label on left

Put the label and description on the left of a settings row.

Receive emails about new products and features.

'use client';

import * as React from 'react';

import { Label } from '@/components/nxds/components/label/label';
import { Switch } from '@/components/nxds/components/switch/switch';

export default function SwitchLabelOnLeft() {
  const switchId = React.useId();
  const descriptionId = React.useId();

  return (
    <div className="nx:flex nx:items-center nx:justify-between nx:gap-4 nx:rounded-lg nx:border-default nx:border-border-default nx:p-4">
      <div className="nx:space-y-0.5">
        <Label htmlFor={switchId}>Marketing emails</Label>
        <p
          id={descriptionId}
          className="nx:typography-body-default nx:text-muted-foreground"
        >
          Receive emails about new products and features.
        </p>
      </div>
      <Switch id={switchId} aria-describedby={descriptionId} />
    </div>
  );
}

Props

Switch

Switch props
PropTypeDefaultDescription
size"default" | "sm" | null"default"No description

Switch turns a setting on or off right away, with no save step. Pair it with a label so the label text is clickable too.

<script setup lang="ts">
import { useId } from 'vue';

import { Switch } from '@/components/switch';

const switchId = useId();
</script>

<template>
  <div class="nx:flex nx:items-center nx:gap-2">
    <Switch :id="switchId" />
    <label :for="switchId">Airplane mode</label>
  </div>
</template>

Installation

  1. Install the following dependencies:

    npm install @vueuse/core@^14.1.0 class-variance-authority@^0.7.1 reka-ui@^2.11.0
    
  2. Copy and paste the following code into your project.

    components/switch/Switch.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import {
      SwitchRoot,
      type SwitchRootEmits,
      type SwitchRootProps,
      SwitchThumb,
      useForwardPropsEmits,
    } from 'reka-ui';
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    import { switchThumbVariants, type SwitchVariants, switchVariants } from '.';
    
    export interface SwitchProps extends SwitchRootProps {
      size?: SwitchVariants['size'];
      class?: HTMLAttributes['class'];
    }
    
    const props = withDefaults(defineProps<SwitchProps>(), { size: 'default' });
    const emits = defineEmits<SwitchRootEmits>();
    
    const delegated = reactiveOmit(props, 'class', 'size');
    const forwarded = useForwardPropsEmits(delegated, emits);
    </script>
    
    <template>
      <SwitchRoot
        v-slot="slotProps"
        v-bind="forwarded"
        data-slot="switch"
        :data-size="size"
        :class="cn(switchVariants({ size }), props.class)"
      >
        <SwitchThumb
          data-slot="switch-thumb"
          :class="switchThumbVariants({ size })"
        >
          <slot name="thumb" v-bind="slotProps" />
        </SwitchThumb>
      </SwitchRoot>
    </template>
    
    components/switch/index.ts
    import { cva, type VariantProps } from 'class-variance-authority';
    
    export { default as Switch, type SwitchProps } from './Switch.vue';
    
    export const switchVariants = cva(
      [
        'nx:peer nx:inline-flex nx:shrink-0 nx:cursor-pointer nx:items-center nx:p-0',
        'nx:rounded-full nx:border-thick nx:border-border-default',
        'nx:transition-control',
        'nx:focus-visible:outline-2 nx:focus-visible:outline-focus-default',
        'nx:aria-invalid:border-error-border nx:aria-invalid:focus-visible:outline-focus-error',
        'nx:aria-invalid:data-[state=checked]:border-primary-background',
        'nx:disabled:cursor-not-allowed nx:disabled:border-border-disabled',
        'nx:data-[state=checked]:border-primary-background nx:data-[state=checked]:bg-primary-background nx:enabled:data-[state=checked]:hover:bg-primary-background-hover nx:data-[state=checked]:disabled:border-primary-disabled nx:data-[state=checked]:disabled:bg-primary-disabled',
        'nx:data-[state=unchecked]:bg-control-background nx:enabled:data-[state=unchecked]:hover:bg-control-background-hover nx:data-[state=unchecked]:disabled:bg-disabled',
      ],
      {
        variants: {
          size: {
            default: 'nx:h-5 nx:w-9',
            sm: 'nx:h-[18px] nx:w-[32px]',
          },
        },
        defaultVariants: {
          size: 'default',
        },
      }
    );
    
    export const switchThumbVariants = cva(
      [
        'nx:pointer-events-none nx:block nx:rounded-full',
        'nx:bg-control-thumb nx:transition-[background-color,transform]',
        'nx:data-[state=checked]:bg-primary-foreground',
        'nx:data-[state=unchecked]:translate-x-0',
      ],
      {
        variants: {
          size: {
            default: 'nx:size-4 nx:data-[state=checked]:translate-x-4',
            sm: 'nx:size-[14px] nx:data-[state=checked]:translate-x-[14px]',
          },
        },
        defaultVariants: {
          size: 'default',
        },
      }
    );
    
    export type SwitchVariants = VariantProps<typeof switchVariants>;
    
  3. Keep each file at the path shown above it.

    Paths are relative to packages/vue/src/. Keep the same folders under your @/ root, and the imports between files work without edits.

Usage

import { Switch } from '@/components/switch';

Examples

Checked

Set default-value to start the switch in the on position.

<script setup lang="ts">
import { Switch } from '@/components/switch';
</script>

<template>
  <Switch aria-label="Toggle switch" :default-value="true" />
</template>

Disabled

A disabled switch shows muted colors and ignores clicks.

<script setup lang="ts">
import { Switch } from '@/components/switch';
</script>

<template>
  <Switch aria-label="Toggle switch" disabled />
</template>

Small

Set size="sm" for dense rows.

<script setup lang="ts">
import { Switch } from '@/components/switch';
</script>

<template>
  <Switch size="sm" aria-label="Toggle switch" />
</template>

Label on left

Put the label and description on the left of a settings row.

<script setup lang="ts">
import { useId } from 'vue';

import { Switch } from '@/components/switch';

const switchId = useId();
const descriptionId = useId();
</script>

<template>
  <div
    class="nx:flex nx:items-center nx:justify-between nx:gap-4 nx:rounded-lg nx:border-default nx:border-border-default nx:p-4"
  >
    <div class="nx:space-y-0.5">
      <label :for="switchId">Marketing emails</label>
      <p
        :id="descriptionId"
        class="nx:typography-body-default nx:text-muted-foreground"
      >
        Receive emails about new products and features.
      </p>
    </div>
    <Switch :id="switchId" :aria-describedby="descriptionId" />
  </div>
</template>

Props

Switch

Switch props
PropTypeDefaultDescription
size"default" | "sm" | null"default"No description