Skip to main content
Nexus Design System
GitHub

Toggle

Toggle is a button that stays pressed until clicked again, such as Bold. Icon-only toggles need an aria-label. For a set of related toggles, use ToggleGroup.

'use client';

import { TextBIcon } from '@phosphor-icons/react/ssr';

import { Toggle } from '@/components/nxds/components/toggle/toggle';

export default function ToggleDefault() {
  return (
    <Toggle aria-label="Bold">
      <TextBIcon />
    </Toggle>
  );
}

This example also needs:

npm install @phosphor-icons/react@^2.1.10

Installation

npx nxds-cli@latest add toggle

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

Usage

import { Toggle } from '@/components/nxds/components/toggle/toggle';

Examples

Variants

Pick default, outline or outline-primary for the border style.

'use client';

import { TextBIcon, TextItalicIcon } from '@phosphor-icons/react/ssr';

import { Toggle } from '@/components/nxds/components/toggle/toggle';

export default function ToggleVariants() {
  return (
    <div className="nx:flex nx:gap-3">
      <Toggle variant="default" aria-label="Bold">
        <TextBIcon />
      </Toggle>
      <Toggle variant="outline" aria-label="Italic">
        <TextItalicIcon />
      </Toggle>
      <Toggle
        variant="outline-primary"
        aria-label="Outline primary bold"
        defaultPressed
      >
        <TextBIcon />
      </Toggle>
    </div>
  );
}

Sizes

Set size to sm, default or lg.

'use client';

import { TextBIcon } from '@phosphor-icons/react/ssr';

import { Toggle } from '@/components/nxds/components/toggle/toggle';

export default function ToggleSizes() {
  return (
    <div className="nx:flex nx:items-center nx:gap-3">
      <Toggle size="sm" aria-label="Small">
        <TextBIcon />
      </Toggle>
      <Toggle size="default" aria-label="Default">
        <TextBIcon />
      </Toggle>
      <Toggle size="lg" aria-label="Large">
        <TextBIcon />
      </Toggle>
    </div>
  );
}

With text

Put an icon and a text label in the same toggle.

'use client';

import { TextBIcon } from '@phosphor-icons/react/ssr';

import { Toggle } from '@/components/nxds/components/toggle/toggle';

export default function ToggleWithText() {
  return (
    <Toggle aria-label="Bold" defaultPressed>
      <TextBIcon />
      Bold
    </Toggle>
  );
}

Disabled

A disabled toggle shows muted colors and ignores clicks.

'use client';

import { TextBIcon } from '@phosphor-icons/react/ssr';

import { Toggle } from '@/components/nxds/components/toggle/toggle';

export default function ToggleDisabled() {
  return (
    <Toggle aria-label="Bold" disabled>
      <TextBIcon />
    </Toggle>
  );
}

Controlled

Drive the pressed state with pressed and onPressedChange.

'use client';

import { useState } from 'react';

import { Toggle } from '@/components/nxds/components/toggle/toggle';

function ControlledBold() {
  const [pressed, setPressed] = useState(false);
  return (
    <Toggle
      variant="outline-primary"
      pressed={pressed}
      onPressedChange={setPressed}
    >
      Controlled bold
    </Toggle>
  );
}

export default function ToggleControlled() {
  return <ControlledBold />;
}

Props

Toggle

Toggle props
PropTypeDefaultDescription
size"default" | "sm" | "lg" | null"default"No description
variant"default" | "outline" | "outline-primary" | null"default"No description

Toggle is a button that stays pressed until clicked again, such as Bold. Icon-only toggles need an aria-label. For a set of related toggles, use ToggleGroup.

<script setup lang="ts">
import { PhTextB } from '@phosphor-icons/vue';

import { Toggle } from '@/components/toggle';
</script>

<template>
  <Toggle aria-label="Bold">
    <PhTextB />
  </Toggle>
</template>

This example also needs:

npm install @phosphor-icons/vue@^2.2.1

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/toggle/Toggle.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import {
      Toggle,
      type ToggleEmits,
      type ToggleProps as RekaToggleProps,
      useForwardPropsEmits,
    } from 'reka-ui';
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    import { type ToggleVariants, toggleVariants } from '.';
    
    export interface ToggleProps extends RekaToggleProps {
      variant?: ToggleVariants['variant'];
      size?: ToggleVariants['size'];
      class?: HTMLAttributes['class'];
    }
    
    const props = withDefaults(defineProps<ToggleProps>(), {
      variant: 'default',
      size: 'default',
    });
    const emits = defineEmits<ToggleEmits>();
    
    const delegated = reactiveOmit(props, 'class', 'variant', 'size');
    const forwarded = useForwardPropsEmits(delegated, emits);
    </script>
    
    <template>
      <Toggle
        v-bind="forwarded"
        data-slot="toggle"
        :data-variant="variant"
        :data-size="size"
        :class="cn(toggleVariants({ variant, size }), props.class)"
      >
        <slot />
      </Toggle>
    </template>
    
    components/toggle/index.ts
    import { cva, type VariantProps } from 'class-variance-authority';
    
    export { default as Toggle, type ToggleProps } from './Toggle.vue';
    
    export const toggleVariants = cva(
      'nx:inline-flex nx:items-center nx:justify-center nx:rounded-md nx:border-0 nx:py-0 nx:whitespace-nowrap nx:text-inherit nx:transition-control nx:hover:bg-background-hover nx:hover:text-foreground nx:focus-visible:outline-2 nx:focus-visible:outline-focus-default nx:disabled:pointer-events-none nx:disabled:text-disabled-foreground nx:aria-invalid:border-error-border nx:aria-invalid:focus-visible:outline-focus-error nx:data-[state=on]:bg-control-background nx:data-[state=on]:text-foreground nx:data-[state=on]:hover:bg-control-background-hover nx:data-[state=on]:disabled:bg-disabled nx:data-[state=on]:disabled:text-disabled-foreground nx:[&_svg]:pointer-events-none nx:[&_svg]:shrink-0 nx:[&_svg]:size-4',
      {
        variants: {
          variant: {
            default: 'nx:bg-transparent',
            outline:
              'nx:border-default nx:border-border-default nx:bg-transparent nx:disabled:border-border-disabled nx:aria-invalid:disabled:border-border-disabled',
            'outline-primary': [
              'nx:border-default nx:border-border-default nx:bg-transparent nx:hover:bg-transparent nx:data-[state=on]:bg-transparent nx:data-[state=on]:hover:bg-transparent nx:data-[state=on]:disabled:bg-transparent',
              'nx:data-[state=off]:not-aria-invalid:hover:border-primary-border nx:not-disabled:data-[state=on]:border-primary-border-active nx:not-disabled:aria-invalid:data-[state=on]:border-border-error-active',
              'nx:disabled:border-border-disabled nx:aria-invalid:disabled:border-border-disabled',
            ],
          },
          size: {
            default: 'nx:px-4 nx:py-2 nx:gap-2 nx:typography-label-default',
            sm: 'nx:px-3 nx:py-1.5 nx:gap-1.5 nx:typography-label-small',
            lg: 'nx:px-8 nx:py-3 nx:gap-2.5 nx:typography-label-default',
          },
        },
        defaultVariants: {
          variant: 'default',
          size: 'default',
        },
      }
    );
    
    export type ToggleVariants = VariantProps<typeof toggleVariants>;
    
  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 { Toggle } from '@/components/toggle';

Examples

Variants

Pick default, outline or outline-primary for the border style.

<script setup lang="ts">
import { PhTextB, PhTextItalic } from '@phosphor-icons/vue';

import { Toggle } from '@/components/toggle';
</script>

<template>
  <div class="nx:flex nx:gap-3">
    <Toggle variant="default" aria-label="Bold">
      <PhTextB />
    </Toggle>
    <Toggle variant="outline" aria-label="Italic">
      <PhTextItalic />
    </Toggle>
    <Toggle
      variant="outline-primary"
      aria-label="Outline primary bold"
      :default-value="true"
    >
      <PhTextB />
    </Toggle>
  </div>
</template>

Sizes

Set size to sm, default or lg.

<script setup lang="ts">
import { PhTextB } from '@phosphor-icons/vue';

import { Toggle } from '@/components/toggle';
</script>

<template>
  <div class="nx:flex nx:items-center nx:gap-3">
    <Toggle size="sm" aria-label="Small">
      <PhTextB />
    </Toggle>
    <Toggle size="default" aria-label="Default">
      <PhTextB />
    </Toggle>
    <Toggle size="lg" aria-label="Large">
      <PhTextB />
    </Toggle>
  </div>
</template>

With text

Put an icon and a text label in the same toggle.

<script setup lang="ts">
import { PhTextB } from '@phosphor-icons/vue';

import { Toggle } from '@/components/toggle';
</script>

<template>
  <Toggle aria-label="Bold" :default-value="true">
    <PhTextB />
    Bold
  </Toggle>
</template>

Disabled

A disabled toggle shows muted colors and ignores clicks.

<script setup lang="ts">
import { PhTextB } from '@phosphor-icons/vue';

import { Toggle } from '@/components/toggle';
</script>

<template>
  <Toggle aria-label="Bold" disabled>
    <PhTextB />
  </Toggle>
</template>

Controlled

Drive the pressed state with v-model.

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

import { Toggle } from '@/components/toggle';

const bold = ref(false);
</script>

<template>
  <Toggle v-model="bold" variant="outline-primary"> Controlled bold </Toggle>
</template>

Props

Toggle

Toggle props
PropTypeDefaultDescription
size"default" | "sm" | "lg" | null"default"No description
variant"default" | "outline" | "outline-primary" | null"default"No description