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/primitives/switch'

Basic

checked + onCheckedChange for controlled, defaultChecked for uncontrolled. onCheckedChange 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 --danger and turns the keyboard ring the danger colour.

Loading example

In a Field

Without label / description only the track renders, so it picks up the enclosing Field's Label, FieldDescription and FieldError. invalid defaults to the field'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
onCheckedChange(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'
invalidbooleanField invalidaria-invalid, data-invalid, danger border and ring
disabledbooleanField / Fieldset 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-2The thumb
--dangerInvalid 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 Field's FieldDescription / FieldError are joined into aria-describedby.
  • invalid sets aria-invalid; keyboard focus shows the shared --ring ring (focus-visible only), danger-coloured 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.