Skip to main content

Switch

A switch flips a setting that takes effect immediately; use a Checkbox for something that is submitted later. It is Headless UI's Switch (a <button role="switch"> with a hidden input for forms) drawn as a track and thumb. With label or description it wraps itself in a Headless UI Field, so clicking the label toggles it and the description lands in aria-describedby.

import Switch from '@zuilib/components/switch'

Basic

checked + onChange for controlled, defaultChecked for uncontrolled. onChange receives the boolean, not an event.

Loading example

Sizes and label position

Track, thumb and thumb travel scale with --spacing (h-4 w-7, h-5 w-9, h-6 w-11); the text beside it is text-sm for sm and md, text-base for lg. labelPosition="start" puts the text first, which suits a settings list where the toggles line up on the right.

Loading example

Invalid

invalid sets aria-invalid, paints the track border --destructive and turns the keyboard ring destructive.

Loading example

In a FormItem

Without label / description only the track renders, so it picks up the enclosing FormItem's Label, Description and Message through the Headless UI Field. invalid defaults to the item's invalid, and disabled (left undefined) inherits from the Field / Fieldset.

Loading example

Props

SwitchProps extends Headless UI SwitchProps (minus as, children) with:

PropTypeDefaultDescription
checkedbooleanControlled state; use defaultChecked for uncontrolled
defaultCheckedbooleanfalse
onChange(checked: boolean) => void
labelReactNodeVisible label; wraps the control in a Field so clicking it toggles
descriptionReactNodeHelper text wired through aria-describedby
labelPosition'end' | 'start''end'Where the label and description sit relative to the track
size'sm' | 'md' | 'lg''md'
invalidbooleanFormItem invalidaria-invalid, data-invalid, destructive border and ring
disabledbooleanField / FormItem disabledLeave unset to inherit; an explicit false overrides the context
namestringNative form participation via a hidden input
valuestring'on'Submitted value when on
idstringReplaces the Field-generated id; the label's htmlFor follows it
classNamestringMerged last onto the track (the element ref points at)

Slots

SlotElementNotes
[data-slot="switch"]buttonThe track, role="switch". Headless UI sets data-checked, data-hover, data-focus, data-disabled; ZUI adds data-invalid
[data-slot="switch-thumb"]spanThe knob; aria-hidden, slides on data-checked
[data-slot="switch-field"]divPresent with label / description: the Headless UI Field
[data-slot="switch-text"]divColumn holding label and description
[data-slot="switch-label"]label
[data-slot="switch-description"]p
/* Consumer CSS: a success-coloured "on" track */
[data-slot="switch"][data-checked] {
background-color: var(--success);
}

Tokens

TokenUsed for
--spacingTrack, thumb and travel per size
--muted, --muted-foregroundOff track and its hover
--primaryOn track
--background, --shadow-mediumThe thumb
--destructiveInvalid border and ring
--ringKeyboard focus ring
--duration-normalTrack colour and thumb travel
--text-sm, --text-baseLabel and description

Accessibility

  • The track is a <button role="switch"> with aria-checked; a hidden input carries name / value to the form.
  • Space and Enter toggle; Tab moves through switches in document order.
  • With label the <label> is associated to the track through the Field, so clicking the label toggles it. Pass the same id on the control when you set one; the label's htmlFor follows it.
  • description and a FormItem's Description / Message are joined into aria-describedby.
  • invalid sets aria-invalid; keyboard focus shows the shared --ring ring (focus-visible only), destructive when invalid.
  • disabled sets data-disabled and aria-disabled, dims the track and label and shows cursor-not-allowed.
  • Thumb travel and track colour transitions are off under prefers-reduced-motion.