Switch

A control that allows the user to toggle between checked and not checked.

Examples

Basic

PropDefaultTypeDescription
checked-booleanThe controlled state of the switch. Can be bind as v-model:checked.
defaultChecked-booleanThe uncontrolled state of the switch.
valueonstringThe value of the switch.
disabledfalsebooleanWhen true, prevents the user from interacting with the switch.
Preview
Code

Form Group

You can use the switch component inside the form-group component, or you can use it with the label component.

Preview
Code

Color

PropDefaultTypeDescription
switchprimarystringChange the color of the switch.
Preview
Code

Size

Adjust the switch size without limits. Use breakpoints (e.g., sm:sm, xs:lg) for responsive sizes or states (e.g., hover:lg, focus:3xl) for state-based sizes.

PropDefaultTypeDescription
sizemdstringChange the size of the switch.
Preview
Code

Icon

Configure the switch icon using the following props.

PropDefaultTypeDescription
icon-stringAdd an icon that appears regardless of state.
checkedIcon-stringAdd an icon that appears when the switch is on.
uncheckedIcon-stringAdd an icon that appears when the switch is off.
Preview
Code

Loading

Set the switch to loading state.

PropDefaultTypeDescription
loading-booleanSet the switch to loading.
Preview
Code

Customization

Configure the progress using the una prop and utility classes.

Preview
Code

Slots

NamePropsDescription
iconcheckedCustomizable icons for the switch in both checked and unchecked states.
loading-iconcheckedThe loading icon slot.

Presets

shortcuts/switch.ts
import type { RuleContext } from 'unocss'
import { parseColor, type Theme } from '@unocss/preset-mini'

type SwitchPrefix = 'switch'

export const staticSwitch: Record<`${SwitchPrefix}-${string}` | SwitchPrefix, string> = {
  // base
  'switch': 'bg-brand switch-focus h-1.25em w-2.25em switch-disabled inline-flex shrink-0 cursor-pointer items-center border-2 border-transparent rounded-full shadow-sm',
  'switch-disabled': 'data-[disabled]:n-disabled',
  'switch-focus': 'focus-visible:ring-brand focus-visible:ring-offset-base transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',

  // thumb
  'switch-thumb': 'square-1em flex items-center justify-center absolute pointer-events-none block border border-base rounded-full bg-base shadow-lg ring-0 transition-transform',
  'switch-thumb-checked': 'translate-x-1.01em',
  'switch-thumb-unchecked': 'translate-x-0.03em',

  // icon
  'switch-icon': 'text-0.7em',
  'switch-icon-unchecked': 'text-muted',
  'switch-icon-checked': 'text-muted',

  // loading
  'switch-loading-icon': 'text-gray animate-spin text-0.8em',
  'switch-loading-icon-name': 'i-loading',
}

export const dynamicSwitch = [
  [
    /^switch-checked(?:-([^-]+))?(?:-([^-]+))?$/,
    ([, color = 'primary'], { theme }: RuleContext<Theme>) => {
      const parsedColor = parseColor(color, theme)
      if ((parsedColor?.cssColor?.type === 'rgb' || parsedColor?.cssColor?.type === 'rgba') && parsedColor.cssColor.components)
        return `data-[state=checked]:n-${color}-600 dark:data-[state=checked]:n-${color}-500`
    },
  ],

  [
    /^switch-unchecked(?:-([^-]+))?(?:-([^-]+))?$/,
    ([, color = 'gray'], { theme }: RuleContext<Theme>) => {
      const parsedColor = parseColor(color, theme)
      if ((parsedColor?.cssColor?.type === 'rgb' || parsedColor?.cssColor?.type === 'rgba') && parsedColor.cssColor.components)
        return `data-[state=unchecked]:n-${color}-200 dark:data-[state=unchecked]:n-${color}-700/58`
    },
  ],
]

export const _switch = [
  ...dynamicSwitch,
  staticSwitch,
]

Props

types/switch.ts
import type { SwitchRootProps } from 'radix-vue'
import type { HTMLAttributes } from 'vue'

export interface NSwitchProps extends SwitchRootProps {
  /**
   * Add a loading indicator to the switch.
   * This will also disable the switch.
   *
   * @default false
   */
  loading?: boolean
  /**
   * Allows you to display an icon. Equivalent of setting both `onIcon` and `offIcon`.
   *
   * @example
   * icon="i-heroicons-question-20-solid"
   */
  icon?: HTMLAttributes['class']
  /**
   * Allows you to display an icon when the switch is on.
   * Accepts icon name and utility classes
   *
   * @example
   * on-icon="i-heroicons-check-20-solid text-white"
   */
  checkedIcon?: HTMLAttributes['class']
  /**
   * Allows you to display an icon when the switch is off.
   * Accepts icon name and utility classes
   *
   * @example
   * off-icon="i-heroicons-x-mark-20-solid text-white"
   */
  uncheckedIcon?: HTMLAttributes['class']
  /**
   * Allows you to change the size of the input.
   *
   * @default md
   *
   * @example
   * size="sm" | size="2cm" | size="2rem" | size="2px"
   */
  size?: HTMLAttributes['class']
  /**
   * Allows you to add `UnaUI` switch preset properties,
   * Think of it as a shortcut for adding options or variants to the preset if available.
   *
   * @example
   * switch-checked="green"
   */
  switchChecked?: HTMLAttributes['class']
  /**
   * Allows you to add `UnaUI` switch preset properties,
   * Think of it as a shortcut for adding options or variants to the preset if available.
   *
   * @example
   * switch-unchecked="gray"
   */
  switchUnchecked?: HTMLAttributes['class']
  /**
   * `UnaUI` preset configuration
   *
   * @see https://github.com/una-ui/una-ui/blob/main/packages/preset/src/_shortcuts/switch.ts
   */
  una?: {
    switch?: HTMLAttributes['class']
    switchChecked?: HTMLAttributes['class']
    switchUnchecked?: HTMLAttributes['class']

    switchThumb?: HTMLAttributes['class']
    switchThumbChecked?: HTMLAttributes['class']
    switchThumbUnchecked?: HTMLAttributes['class']

    switchIcon?: HTMLAttributes['class']
    switchIconChecked?: HTMLAttributes['class']
    switchIconUnchecked?: HTMLAttributes['class']

    switchLoading?: HTMLAttributes['class']
    switchLoadingIconName?: HTMLAttributes['class']
  }
}

Components

Switch.vue
<script setup lang="ts">
import type { NSwitchProps } from '../../types'
import { reactivePick } from '@vueuse/core'
import {
  SwitchRoot,
  type SwitchRootEmits,
  SwitchThumb,
  useForwardPropsEmits,
} from 'radix-vue'
import { cn } from '../../utils'
import NIcon from '../elements/Icon.vue'

const props = withDefaults(defineProps<NSwitchProps>(), {
  size: 'md',
  switchChecked: '~',
  switchUnchecked: '~',
})

const emit = defineEmits<SwitchRootEmits>()

const rootPropsEmits = useForwardPropsEmits(reactivePick(props, 'as', 'asChild', 'checked', 'defaultChecked', 'disabled', 'id', 'name', 'required', 'value'), emit)
</script>

<template>
  <SwitchRoot
    v-slot="{ checked }"
    v-bind="rootPropsEmits"
    :class="cn(
      'peer switch',
      una?.switch,
      checked
        ? una?.switchChecked
        : una?.switchUnchecked,
    )"
    :disabled="disabled || loading"
    :switch-checked
    :switch-unchecked
    :size
  >
    <SwitchThumb
      :class="cn(
        'switch-thumb',
        una?.switchThumb,
        checked
          ? 'switch-thumb-checked'
          : 'switch-thumb-unchecked',
        checked
          ? una?.switchThumbChecked
          : una?.switchThumbUnchecked,
      )"
    >
      <slot v-if="!loading" name="icon" :checked>
        <NIcon
          :name="(checked ? checkedIcon : uncheckedIcon) || icon"
          :class="cn(
            'switch-icon',
            una?.switchIcon,
            checked
              ? ['switch-icon-checked', una?.switchIconChecked]
              : ['switch-icon-unchecked', una?.switchIconUnchecked],
          )"
        />
      </slot>
      <slot v-else name="loading-icon" :checked>
        <NIcon
          :class="cn(
            'switch-icon switch-loading-icon',
            una?.switchLoading,
          )"
          :name="una?.switchLoadingIconName || 'switch-loading-icon-name'"
        />
      </slot>
    </SwitchThumb>
  </SwitchRoot>
</template>