Switch
An on and off toggle for settings, with the label on the left and the control on the right.
A declined renewal is retried up to 4 times over 14 days.
'use client'
import { useState } from 'react'
import { Description, Label } from '@/components/fieldset'
import { Switch, SwitchField } from '@/components/switch'
export default function Example() {
let [retry, setRetry] = useState(true)
return (
<SwitchField>
<Label>Retry failed payments</Label>
<Description>A declined renewal is retried up to 4 times over 14 days.</Description>
<Switch name="smart_retries" checked={retry} onChange={setRetry} />
</SwitchField>
)
}<script setup lang="ts">
import Description from '@/components/description.vue'
import Label from '@/components/label.vue'
import Switch from '@/components/switch.vue'
import SwitchField from '@/components/switch-field.vue'
import { ref } from 'vue'
const retry = ref(true)
</script>
<template>
<SwitchField>
<Label>Retry failed payments</Label>
<Description>A declined renewal is retried up to 4 times over 14 days.</Description>
<Switch name="smart_retries" v-model="retry" />
</SwitchField>
</template>Installation
Copy these files into the folder where you keep your components. They import each other with relative paths, so the folder can live anywhere. The packages they rely on are listed on the Installation page.
import * as Headless from '@headlessui/react'
import clsx from 'clsx'
import type React from 'react'
export function SwitchGroup({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
return (
<div
data-slot="control"
{...props}
className={clsx(
className,
'space-y-3 has-data-[slot=description]:space-y-6',
'**:data-[slot=label]:font-normal',
'has-data-[slot=description]:**:data-[slot=label]:font-medium'
)}
/>
)
}
const fieldGrid = 'grid gap-x-8 gap-y-1 grid-cols-[1fr_auto] sm:grid-cols-[1fr_auto]'
const fieldColumns = '*:data-[slot=control]:col-start-2 *:data-[slot=label]:col-start-1 *:data-[slot=description]:col-start-1'
const fieldRows = '*:data-[slot=label]:row-start-1 *:data-[slot=description]:row-start-2'
const controlAlignment = '*:data-[slot=control]:self-start sm:*:data-[slot=control]:mt-0.5'
export function SwitchField({ className, ...props }: { className?: string } & Omit<Headless.FieldProps, 'as' | 'className'>) {
return (
<Headless.Field
data-slot="field"
{...props}
className={clsx(className, fieldGrid, fieldColumns, fieldRows, controlAlignment, 'has-data-[slot=description]:**:data-[slot=label]:font-medium')}
/>
)
}
type SwitchColors = {
background: string
ring: string
thumb: string
thumbRing: string
thumbShadow: string
dark?: Partial<Pick<SwitchColors, 'background' | 'ring' | 'thumb' | 'thumbRing'>>
}
const withOpacity = (color: string, percent: number) => `color-mix(in oklab, ${color} ${percent}%, transparent)`
const palette = (hue: string) => (shade: number) => `var(--color-${hue}-${shade})`
const accent = (shade: number) => `var(--accent-${shade},var(--color-blue-${shade}))`
const zinc = palette('zinc')
const black = 'var(--color-black)'
function whiteThumb(color: (shade: number) => string, ring: number, background: number, opacity = 90): SwitchColors {
return {
background: color(background),
ring: withOpacity(color(ring), opacity),
thumb: 'white',
thumbRing: withOpacity(color(ring), opacity),
thumbShadow: withOpacity(color(900), 20),
}
}
function darkThumb(color: (shade: number) => string, ring: number, background: number): SwitchColors {
return {
background: color(background),
ring: withOpacity(color(ring), 80),
thumb: color(950),
thumbRing: 'transparent',
thumbShadow: 'transparent',
}
}
const darkSwitch = {
background: zinc(900),
ring: withOpacity(zinc(950), 90),
thumb: 'white',
thumbRing: withOpacity(zinc(950), 90),
thumbShadow: withOpacity(black, 10),
} satisfies SwitchColors
const colors = {
'dark/zinc': { ...darkSwitch, dark: { background: withOpacity('var(--color-white)', 25), thumbRing: withOpacity(zinc(700), 90) } },
'dark/white': { ...darkSwitch, dark: { background: 'var(--color-white)', thumb: zinc(900), thumbRing: 'transparent' } },
dark: { ...darkSwitch, dark: { ring: withOpacity('var(--color-white)', 15) } },
zinc: { ...whiteThumb(zinc, 700, 600), thumbShadow: withOpacity(black, 10) },
white: {
background: 'white',
ring: withOpacity(black, 15),
thumb: zinc(950),
thumbRing: 'transparent',
thumbShadow: withOpacity(black, 10),
},
red: whiteThumb(palette('red'), 700, 600),
orange: whiteThumb(palette('orange'), 600, 500),
amber: darkThumb(palette('amber'), 500, 400),
yellow: darkThumb(palette('yellow'), 400, 300),
lime: darkThumb(palette('lime'), 400, 300),
green: whiteThumb(palette('green'), 700, 600),
emerald: whiteThumb(palette('emerald'), 600, 500),
teal: whiteThumb(palette('teal'), 700, 600),
cyan: darkThumb(palette('cyan'), 400, 300),
sky: whiteThumb(palette('sky'), 600, 500, 80),
blue: whiteThumb(palette('blue'), 700, 600),
accent: whiteThumb(accent, 700, 600),
indigo: whiteThumb(palette('indigo'), 600, 500),
violet: whiteThumb(palette('violet'), 600, 500),
purple: whiteThumb(palette('purple'), 600, 500),
fuchsia: whiteThumb(palette('fuchsia'), 600, 500),
pink: whiteThumb(palette('pink'), 600, 500),
rose: whiteThumb(palette('rose'), 600, 500),
} satisfies Record<string, SwitchColors>
type Color = keyof typeof colors
function cssVariables(colors: SwitchColors) {
return {
'--checked-track': colors.background,
'--checked-track-ring': colors.ring,
'--checked-thumb': colors.thumb,
'--checked-thumb-ring': colors.thumbRing,
'--checked-thumb-shadow': colors.thumbShadow,
'--dark-checked-track': colors.dark?.background,
'--dark-checked-track-ring': colors.dark?.ring,
'--dark-checked-thumb': colors.dark?.thumb,
'--dark-checked-thumb-ring': colors.dark?.thumbRing,
} as React.CSSProperties
}
const themeColors = [
'[--track-background:var(--checked-track)] dark:[--track-background:var(--dark-checked-track,var(--checked-track))]',
'[--track-ring:var(--checked-track-ring)] dark:[--track-ring:var(--dark-checked-track-ring,transparent)]',
'[--thumb-background:var(--checked-thumb)] dark:[--thumb-background:var(--dark-checked-thumb,var(--checked-thumb))]',
'[--thumb-ring:var(--checked-thumb-ring)] dark:[--thumb-ring:var(--dark-checked-thumb-ring,var(--checked-thumb-ring))]',
'[--thumb-shadow:var(--checked-thumb-shadow)]',
]
const trackLayout = [
'group relative isolate inline-flex rounded-full p-[3px] cursor-default',
'h-6 w-10 sm:h-5 sm:w-8',
]
const trackTransition = 'transition ease-in-out duration-0 data-changing:duration-200'
const trackBackground = [
'bg-zinc-200 dark:bg-white/5',
'ring-1 ring-inset ring-black/5 dark:ring-white/15',
'data-hover:ring-black/15 dark:data-hover:ring-white/25',
]
const trackChecked = [
'data-checked:bg-(--track-background) dark:data-checked:bg-(--track-background)',
'data-checked:ring-(--track-ring) dark:data-checked:ring-(--track-ring)',
'data-hover:data-checked:ring-(--track-ring) dark:data-hover:data-checked:ring-(--track-ring)',
]
const trackFocus = 'data-focus:outline-accent-500 data-focus:outline-2 data-focus:outline-offset-2 focus:not-data-focus:outline-hidden'
const trackDisabled = [
'data-disabled:opacity-50',
'data-disabled:bg-zinc-200 dark:data-disabled:bg-white/15',
'data-disabled:data-checked:bg-zinc-200 dark:data-disabled:data-checked:bg-white/15',
'data-disabled:data-checked:ring-black/5 dark:data-disabled:data-checked:ring-white/15',
]
const trackForcedColors = [
'forced-colors:outline',
'forced-colors:[--track-background:Highlight] dark:forced-colors:[--track-background:Highlight]',
]
const track = [trackLayout, trackTransition, trackBackground, trackChecked, trackFocus, trackDisabled, trackForcedColors, themeColors]
const thumbLayout = 'pointer-events-none relative inline-block rounded-full size-4.5 sm:size-3.5'
const forcedColorsOutline = 'border border-transparent'
const thumbMotion = [
'transition ease-in-out duration-200 translate-x-0',
'sm:group-data-checked:translate-x-3 group-data-checked:translate-x-4',
]
const thumbBackground = [
'bg-white shadow-sm ring-1 ring-black/5',
'group-data-checked:bg-(--thumb-background) group-data-checked:ring-(--thumb-ring)',
'group-data-checked:shadow-(--thumb-shadow)',
]
const thumbDisabled = [
'group-data-checked:group-data-disabled:bg-white',
'group-data-checked:group-data-disabled:ring-black/5',
'group-data-checked:group-data-disabled:shadow-sm',
]
const thumb = [thumbLayout, forcedColorsOutline, thumbMotion, thumbBackground, thumbDisabled]
type SwitchProps = {
color?: Color
className?: string
} & Omit<Headless.SwitchProps, 'as' | 'className' | 'children'>
export function Switch({ color = 'accent', className, style, ...props }: SwitchProps) {
return (
<Headless.Switch
data-slot="control"
{...props}
style={{ ...cssVariables(colors[color]), ...style }}
className={clsx(className, track)}
>
<span aria-hidden="true" className={clsx(thumb)} />
</Headless.Switch>
)
}<script setup lang="ts">
import { SwitchRoot } from 'reka-ui'
import { computed, onBeforeUnmount, ref, useAttrs, watch, type CSSProperties } from 'vue'
import { useDisabled, useFieldControl } from './fields'
import { vInteractions } from './interactions'
defineOptions({ inheritAttrs: false })
type SwitchColors = {
background: string
ring: string
thumb: string
thumbRing: string
thumbShadow: string
dark?: Partial<Pick<SwitchColors, 'background' | 'ring' | 'thumb' | 'thumbRing'>>
}
const withOpacity = (color: string, percent: number) => `color-mix(in oklab, ${color} ${percent}%, transparent)`
const palette = (hue: string) => (shade: number) => `var(--color-${hue}-${shade})`
const accent = (shade: number) => `var(--accent-${shade},var(--color-blue-${shade}))`
const zinc = palette('zinc')
const black = 'var(--color-black)'
function whiteThumb(color: (shade: number) => string, ring: number, background: number, opacity = 90): SwitchColors {
return {
background: color(background),
ring: withOpacity(color(ring), opacity),
thumb: 'white',
thumbRing: withOpacity(color(ring), opacity),
thumbShadow: withOpacity(color(900), 20),
}
}
function darkThumb(color: (shade: number) => string, ring: number, background: number): SwitchColors {
return {
background: color(background),
ring: withOpacity(color(ring), 80),
thumb: color(950),
thumbRing: 'transparent',
thumbShadow: 'transparent',
}
}
const darkSwitch = {
background: zinc(900),
ring: withOpacity(zinc(950), 90),
thumb: 'white',
thumbRing: withOpacity(zinc(950), 90),
thumbShadow: withOpacity(black, 10),
} satisfies SwitchColors
const colors = {
'dark/zinc': { ...darkSwitch, dark: { background: withOpacity('var(--color-white)', 25), thumbRing: withOpacity(zinc(700), 90) } },
'dark/white': { ...darkSwitch, dark: { background: 'var(--color-white)', thumb: zinc(900), thumbRing: 'transparent' } },
dark: { ...darkSwitch, dark: { ring: withOpacity('var(--color-white)', 15) } },
zinc: { ...whiteThumb(zinc, 700, 600), thumbShadow: withOpacity(black, 10) },
white: {
background: 'white',
ring: withOpacity(black, 15),
thumb: zinc(950),
thumbRing: 'transparent',
thumbShadow: withOpacity(black, 10),
},
red: whiteThumb(palette('red'), 700, 600),
orange: whiteThumb(palette('orange'), 600, 500),
amber: darkThumb(palette('amber'), 500, 400),
yellow: darkThumb(palette('yellow'), 400, 300),
lime: darkThumb(palette('lime'), 400, 300),
green: whiteThumb(palette('green'), 700, 600),
emerald: whiteThumb(palette('emerald'), 600, 500),
teal: whiteThumb(palette('teal'), 700, 600),
cyan: darkThumb(palette('cyan'), 400, 300),
sky: whiteThumb(palette('sky'), 600, 500, 80),
blue: whiteThumb(palette('blue'), 700, 600),
accent: whiteThumb(accent, 700, 600),
indigo: whiteThumb(palette('indigo'), 600, 500),
violet: whiteThumb(palette('violet'), 600, 500),
purple: whiteThumb(palette('purple'), 600, 500),
fuchsia: whiteThumb(palette('fuchsia'), 600, 500),
pink: whiteThumb(palette('pink'), 600, 500),
rose: whiteThumb(palette('rose'), 600, 500),
} satisfies Record<string, SwitchColors>
type Color = keyof typeof colors
function cssVariables(colors: SwitchColors) {
return {
'--checked-track': colors.background,
'--checked-track-ring': colors.ring,
'--checked-thumb': colors.thumb,
'--checked-thumb-ring': colors.thumbRing,
'--checked-thumb-shadow': colors.thumbShadow,
'--dark-checked-track': colors.dark?.background,
'--dark-checked-track-ring': colors.dark?.ring,
'--dark-checked-thumb': colors.dark?.thumb,
'--dark-checked-thumb-ring': colors.dark?.thumbRing,
} as CSSProperties
}
const themeColors = [
'[--track-background:var(--checked-track)] dark:[--track-background:var(--dark-checked-track,var(--checked-track))]',
'[--track-ring:var(--checked-track-ring)] dark:[--track-ring:var(--dark-checked-track-ring,transparent)]',
'[--thumb-background:var(--checked-thumb)] dark:[--thumb-background:var(--dark-checked-thumb,var(--checked-thumb))]',
'[--thumb-ring:var(--checked-thumb-ring)] dark:[--thumb-ring:var(--dark-checked-thumb-ring,var(--checked-thumb-ring))]',
'[--thumb-shadow:var(--checked-thumb-shadow)]',
]
const trackLayout = [
'group relative isolate inline-flex rounded-full p-[3px] cursor-default',
'h-6 w-10 sm:h-5 sm:w-8',
]
const trackTransition = 'transition ease-in-out duration-0 data-changing:duration-200'
const trackBackground = [
'bg-zinc-200 dark:bg-white/5',
'ring-1 ring-inset ring-black/5 dark:ring-white/15',
'data-hover:ring-black/15 dark:data-hover:ring-white/25',
]
const trackChecked = [
'data-checked:bg-(--track-background) dark:data-checked:bg-(--track-background)',
'data-checked:ring-(--track-ring) dark:data-checked:ring-(--track-ring)',
'data-hover:data-checked:ring-(--track-ring) dark:data-hover:data-checked:ring-(--track-ring)',
]
const trackFocus = 'data-focus:outline-accent-500 data-focus:outline-2 data-focus:outline-offset-2 focus:not-data-focus:outline-hidden'
const trackDisabled = [
'data-disabled:opacity-50',
'data-disabled:bg-zinc-200 dark:data-disabled:bg-white/15',
'data-disabled:data-checked:bg-zinc-200 dark:data-disabled:data-checked:bg-white/15',
'data-disabled:data-checked:ring-black/5 dark:data-disabled:data-checked:ring-white/15',
]
const trackForcedColors = [
'forced-colors:outline',
'forced-colors:[--track-background:Highlight] dark:forced-colors:[--track-background:Highlight]',
]
const track = [trackLayout, trackTransition, trackBackground, trackChecked, trackFocus, trackDisabled, trackForcedColors, themeColors]
const thumbLayout = 'pointer-events-none relative inline-block rounded-full size-4.5 sm:size-3.5'
const forcedColorsOutline = 'border border-transparent'
const thumbMotion = [
'transition ease-in-out duration-200 translate-x-0',
'sm:group-data-checked:translate-x-3 group-data-checked:translate-x-4',
]
const thumbBackground = [
'bg-white shadow-sm ring-1 ring-black/5',
'group-data-checked:bg-(--thumb-background) group-data-checked:ring-(--thumb-ring)',
'group-data-checked:shadow-(--thumb-shadow)',
]
const thumbDisabled = [
'group-data-checked:group-data-disabled:bg-white',
'group-data-checked:group-data-disabled:ring-black/5',
'group-data-checked:group-data-disabled:shadow-sm',
]
const thumb = [thumbLayout, forcedColorsOutline, thumbMotion, thumbBackground, thumbDisabled]
const props = withDefaults(
defineProps<{
color?: Color
disabled?: boolean
}>(),
{ color: 'accent' }
)
const model = defineModel<boolean>({ default: false })
const attrs = useAttrs()
const fieldControl = useFieldControl(attrs)
const isDisabled = useDisabled(() => props.disabled)
const changing = ref(false)
let changingTimer: ReturnType<typeof setTimeout> | undefined
watch(model, () => {
changing.value = true
clearTimeout(changingTimer)
changingTimer = setTimeout(() => (changing.value = false), 200)
})
onBeforeUnmount(() => clearTimeout(changingTimer))
const innerAttrs = computed(() => {
const { class: _class, ...rest } = attrs
return rest
})
</script>
<template>
<SwitchRoot
v-bind="innerAttrs"
v-interactions
v-model="model"
data-slot="control"
:disabled="isDisabled"
:id="fieldControl.id.value"
:aria-describedby="fieldControl.describedBy.value"
:data-checked="model ? '' : undefined"
:data-changing="changing ? '' : undefined"
:data-disabled="isDisabled ? '' : undefined"
:style="[cssVariables(colors[color]), attrs.style as CSSProperties]"
:class="[attrs.class, track]"
>
<span aria-hidden="true" :class="thumb" />
</SwitchRoot>
</template>Component API
| Prop | Default | Description |
|---|---|---|
Switchbuilt on the Headless UI <Headless.Switch> component | ||
checked | false | On state when you control it yourself. |
onChange | - | Receives the new boolean each time the switch is flipped. |
color | accent | Track color while the switch is on. |
disabled | false | Blocks interaction and dims the switch. |
SwitchFieldbuilt on the Headless UI <Headless.Field> component | ||
disabled | false | Disables the switch and dims its label and description. |
SwitchGrouprenders a <div> element | ||
| This component does not expose any component-specific props. | ||
| Prop | Default | Description |
|---|---|---|
Switchbuilt on the Reka UI <SwitchRoot> component | ||
v-model | false | Two-way binding of the on state. |
color | accent | Track color while the switch is on. |
disabled | false | Blocks interaction and dims the switch. |
SwitchFieldrenders a <div> element | ||
disabled | false | Disables the switch and dims its label and description. |
SwitchGrouprenders a <div> element | ||
| This component does not expose any component-specific props. | ||
Usage
People expect a switch to act the moment it flips, the way a light switch does, so save the change right away instead of waiting for a submit button. If the choice only counts once a form is sent, use a checkbox. Write the label as the feature being turned on, not as a question.
Label a setting
SwitchField puts the Label on the left, the Switch on the right, and connects them so the text toggles the switch as well:
'use client'
import { useState } from 'react'
import { Label } from '@/components/fieldset'
import { Switch, SwitchField } from '@/components/switch'
export default function Example() {
let [testMode, setTestMode] = useState(false)
return (
<SwitchField>
<Label>Test mode</Label>
<Switch name="test_mode" checked={testMode} onChange={setTestMode} />
</SwitchField>
)
}<script setup lang="ts">
import Label from '@/components/label.vue'
import Switch from '@/components/switch.vue'
import SwitchField from '@/components/switch-field.vue'
import { ref } from 'vue'
const testMode = ref(false)
</script>
<template>
<SwitchField>
<Label>Test mode</Label>
<Switch name="test_mode" v-model="testMode" />
</SwitchField>
</template>Add a line of context
A Description under the label explains who or what the setting affects. The label moves to medium weight to stay the first thing people read:
Customers can download past invoices and update their card.
<SwitchField>
<Label>Customer portal</Label>
<Description>Customers can download past invoices and update their card.</Description>
<Switch name="portal" checked={portal} onChange={setPortal} />
</SwitchField><SwitchField>
<Label>Customer portal</Label>
<Description>Customers can download past invoices and update their card.</Description>
<Switch name="portal" v-model="portal" />
</SwitchField>Stack several settings
SwitchGroup spaces a list of fields evenly, with more room between them once they carry descriptions:
<SwitchGroup>
<SwitchField>
<Label>Weekly revenue digest</Label>
<Switch name="weekly_digest" checked={digest} onChange={setDigest} />
</SwitchField>
<SwitchField>
<Label>Payout sent</Label>
<Switch name="payout_sent" checked={payouts} onChange={setPayouts} />
</SwitchField>
</SwitchGroup><SwitchGroup>
<SwitchField>
<Label>Weekly revenue digest</Label>
<Switch name="weekly_digest" v-model="digest" />
</SwitchField>
<SwitchField>
<Label>Payout sent</Label>
<Switch name="payout_sent" v-model="payouts" />
</SwitchField>
</SwitchGroup>Group settings under a heading
A settings page usually splits switches by topic. Put each topic in a Fieldset with a Legend and, when needed, a Text that says who it applies to:
'use client'
import { useState } from 'react'
import { Description, Fieldset, Label, Legend } from '@/components/fieldset'
import { Switch, SwitchField, SwitchGroup } from '@/components/switch'
import { Text } from '@/components/text'
export default function Example() {
let [newKey, setNewKey] = useState(true)
let [newMember, setNewMember] = useState(false)
return (
<Fieldset>
<Legend>Security alerts</Legend>
<Text>Sent to every owner of the northwind workspace.</Text>
<SwitchGroup>
<SwitchField>
<Label>API key created</Label>
<Description>Includes the key name and who created it.</Description>
<Switch name="alert_api_key" checked={newKey} onChange={setNewKey} />
</SwitchField>
<SwitchField>
<Label>Member invited</Label>
<Description>Includes the email address and the role granted.</Description>
<Switch name="alert_member" checked={newMember} onChange={setNewMember} />
</SwitchField>
</SwitchGroup>
</Fieldset>
)
}<script setup lang="ts">
import Description from '@/components/description.vue'
import Fieldset from '@/components/fieldset.vue'
import Label from '@/components/label.vue'
import Legend from '@/components/legend.vue'
import Switch from '@/components/switch.vue'
import SwitchField from '@/components/switch-field.vue'
import SwitchGroup from '@/components/switch-group.vue'
import Text from '@/components/text.vue'
import { ref } from 'vue'
const newKey = ref(true)
const newMember = ref(false)
</script>
<template>
<Fieldset>
<Legend>Security alerts</Legend>
<Text>Sent to every owner of the northwind workspace.</Text>
<SwitchGroup>
<SwitchField>
<Label>API key created</Label>
<Description>Includes the key name and who created it.</Description>
<Switch name="alert_api_key" v-model="newKey" />
</SwitchField>
<SwitchField>
<Label>Member invited</Label>
<Description>Includes the email address and the role granted.</Description>
<Switch name="alert_member" v-model="newMember" />
</SwitchField>
</SwitchGroup>
</Fieldset>
</template>States
Turn a setting on from the start
Use defaultChecked when the switch should start on and you do not need to track it in state:
Start the v-model ref at true to render the switch on from the start:
<SwitchField>
<Label>Send invoice reminders</Label>
<Switch name="reminders" defaultChecked />
</SwitchField><script setup lang="ts">
import { ref } from 'vue'
const reminders = ref(true)
</script>
<template>
<SwitchField>
<Label>Send invoice reminders</Label>
<Switch name="reminders" v-model="reminders" />
</SwitchField>
</template>Lock a setting for some roles
disabled on the SwitchField freezes the switch in its current position and dims the text. Use the description to say who can change it:
Only workspace owners can change this.
<SwitchField disabled>
<Label>Require two-factor sign-in</Label>
<Description>Only workspace owners can change this.</Description>
<Switch name="require_2fa" defaultChecked />
</SwitchField><SwitchField disabled>
<Label>Require two-factor sign-in</Label>
<Description>Only workspace owners can change this.</Description>
<Switch name="require_2fa" v-model="require2fa" />
</SwitchField>A switch used without a field takes disabled directly.
Forms
Keep the state in your own code
Pass checked and onChange when flipping the switch should update other parts of the screen or call your API:
Bind a ref with v-model when flipping the switch should update other parts of the screen or call your API:
Showing test data
'use client'
import { useState } from 'react'
import { Label } from '@/components/fieldset'
import { Switch, SwitchField } from '@/components/switch'
export default function Example() {
let [testMode, setTestMode] = useState(true)
return (
<>
<SwitchField>
<Label>Test mode</Label>
<Switch checked={testMode} onChange={setTestMode} />
</SwitchField>
<p>{testMode ? 'Showing test data' : 'Showing live data'}</p>
</>
)
}<script setup lang="ts">
import Label from '@/components/label.vue'
import Switch from '@/components/switch.vue'
import SwitchField from '@/components/switch-field.vue'
import { ref } from 'vue'
const testMode = ref(true)
</script>
<template>
<SwitchField>
<Label>Test mode</Label>
<Switch v-model="testMode" />
</SwitchField>
<p>{{ testMode ? 'Showing test data' : 'Showing live data' }}</p>
</template>Choose what the form submits
Give the switch a name to include it in a native form. value replaces the default on sent while the switch is on:
<SwitchField>
<Label>Collect tax automatically</Label>
<Switch name="tax_behavior" value="automatic" />
</SwitchField>Customization
Change the track color
Switches follow your theme accent, and fall back to blue when none is set. Pass color for a setting that deserves its own signal:
<SwitchField>
<Label>Accept live payments</Label>
<Switch color="emerald" checked={live} onChange={setLive} />
</SwitchField><SwitchField>
<Label>Accept live payments</Label>
<Switch color="emerald" v-model="live" />
</SwitchField>Available colors
dark/zinc and dark/white swap shades between light and dark mode. The other 20 names keep the same track in both:
<Switch color="dark/zinc" checked={checked} onChange={setChecked} />
<Switch color="dark/white" checked={checked} onChange={setChecked} />
<Switch color="teal" checked={checked} onChange={setChecked} /><Switch color="dark/zinc" v-model="checked" />
<Switch color="dark/white" v-model="checked" />
<Switch color="teal" v-model="checked" />Put the switch before its label
Inside a filter bar or a table toolbar, the switch often comes first. Render it alone and link your own <label> to it with id and htmlFor:
Inside a filter bar or a table toolbar, the switch often comes first. Render it alone and link your own <label> to it with id and for:
<div className="flex items-center gap-4">
<Switch id="archived" checked={enabled} onChange={setEnabled} />
<label htmlFor="archived" className="text-base/6 text-zinc-950 select-none sm:text-sm/6 dark:text-white">
Include archived customers
</label>
</div><div class="flex items-center gap-4">
<Switch id="archived" v-model="enabled" />
<label for="archived" class="text-base/6 text-zinc-950 select-none sm:text-sm/6 dark:text-white">
Include archived customers
</label>
</div>Accessibility
In a SwitchField, the Label names the switch and the Description is announced with it. A switch rendered on its own has no name until you give it one.
Name a switch without visible text
In a list of webhook endpoints, the URL in the row is not linked to the switch. Repeat it in an aria-label:
<Switch
name="endpoint_enabled"
checked={enabled}
onChange={setEnabled}
aria-label="Enable https://api.northwind.io/hooks"
/><Switch name="endpoint_enabled" v-model="enabled" aria-label="Enable https://api.northwind.io/hooks" />