UIPackage

Toggle

Vue action
Edit on GitHub

On/off button (different from Switch — this is shaped like a button and lives in toolbars). Three sizes, two variants. Use inside Toggle Group or standalone for "press to enable" buttons like bold / italic.

Also available for React ->

Installation

$ npx shadcn-vue@latest add https://uipkge.dev/r/vue/toggle.json

Or with the named registry: npx shadcn-vue@latest add @uipkge/toggle

Examples

Props

Name Type / Values Default Required
class HTMLAttributes['class'] optional
variant
'default''outline'
'default' optional
size
'default''sm''lg'
'default' optional
asChild boolean optional
as string | object optional
defaultValue boolean optional
modelValue boolean optional
pressed boolean optional
disabled boolean false optional
name string optional
required boolean optional

Dependencies

Used by

Files (3)

  • app/components/ui/toggle/Toggle.vue 2 kB
    <script setup lang="ts">
    import type { ToggleEmits } from 'reka-ui'
    import type { HTMLAttributes } from 'vue'
    import { computed, getCurrentInstance } from 'vue'
    import { reactiveOmit } from '@vueuse/core'
    import { Toggle, useForwardPropsEmits } from 'reka-ui'
    import { cn } from '@/lib/utils'
    import { toggleVariants } from './toggle.variants'
    
    // Inlined unions for variant/size: Vue's SFC compiler can't extract
    // runtime props from indexed-access types like `ToggleVariants['variant']`,
    // so we spell the unions out. cva still validates at runtime.
    // Same goes for `ToggleProps` from reka-ui (no exports.types condition);
    // we inline only the props we actually expose.
    const props = withDefaults(
      defineProps<{
        class?: HTMLAttributes['class']
        variant?: 'default' | 'outline'
        size?: 'default' | 'sm' | 'lg'
        asChild?: boolean
        as?: string | object
        defaultValue?: boolean
        modelValue?: boolean
        pressed?: boolean
        disabled?: boolean
        name?: string
        required?: boolean
      }>(),
      {
        variant: 'default',
        size: 'default',
        disabled: false,
      },
    )
    
    const emits = defineEmits<ToggleEmits>()
    
    const delegatedProps = reactiveOmit(props, 'class', 'size', 'variant', 'modelValue')
    const forwarded = useForwardPropsEmits(delegatedProps, emits)
    
    const instance = getCurrentInstance()
    const isControlled = computed(() => Boolean(instance?.vnode?.props?.['onUpdate:modelValue']))
    const userPassedModelValue = computed(() => {
      const raw = instance?.vnode?.props
      return Boolean(raw && ('modelValue' in raw || 'model-value' in raw))
    })
    
    const toggleStateBindings = computed(() => {
      if (isControlled.value) return { 'model-value': props.modelValue }
      if (userPassedModelValue.value) return { 'default-value': Boolean(props.modelValue) }
      return { 'default-value': false }
    })
    </script>
    
    <template>
      <Toggle
        v-slot="slotProps"
        data-uipkge
        data-slot="toggle"
        v-bind="{ ...forwarded, ...toggleStateBindings }"
        :class="cn(toggleVariants({ variant, size }), props.class)"
      >
        <slot v-bind="slotProps" />
      </Toggle>
    </template>
  • app/components/ui/toggle/toggle.variants.ts 1.6 kB
    import type { VariantProps } from 'class-variance-authority'
    import { cva } from 'class-variance-authority'
    
    /**
     * Variant definitions live in their own file (rather than the package
     * `index.ts`) so consuming Vue SFCs can import without creating a circular
     * dependency through the index. See card.variants.ts for the canonical
     * example + the SSR symptom that motivated the split.
     */
    
    export const toggleVariants = cva(
      // Focus ring intentionally subtle: 1px ring on :focus-visible only, no
      // border swap. Keyboard users still get a clear focused state; mouse
      // users don't see a loud 3px halo after click (which lingers because
      // the button retains focus). Matches the segmented-control density.
      "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:ring-ring/50 focus-visible:ring-1 outline-none transition-colors duration-200 whitespace-nowrap",
      {
        variants: {
          variant: {
            default: 'bg-transparent',
            outline: 'border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground',
          },
          size: {
            default: 'h-9 px-2 min-w-9',
            sm: 'h-8 px-1.5 min-w-8',
            lg: 'h-10 px-2.5 min-w-10',
          },
        },
        defaultVariants: {
          variant: 'default',
          size: 'default',
        },
      },
    )
    
    export type ToggleVariants = VariantProps<typeof toggleVariants>
  • app/components/ui/toggle/index.ts 0.3 kB
    export { default as Toggle } from './Toggle.vue'
    
    // Re-export variant API from the sibling file (kept separate to avoid the
    // Component.vue <-> index.ts circular import that broke dev SSR for Card).
    export { toggleVariants, type ToggleVariants } from './toggle.variants'

Raw manifest: https://uipkge.dev/r/vue/toggle.json