- W3C pattern: APG Switch,
over the normative
WAI-ARIA 1.2
switchrole definition. - State machine: built on
@dunky.dev/state-machine. - Prior art: API shape modeled on the Radix, Base UI, and Ark switches.
A switch is a binary control that turns a setting on or off, taking effect immediately β airplane mode, notifications, dark theme. Unlike a checkbox it never collects a choice for later submission and never has a mixed state: it is always exactly on or off.
<Switch> β root; owns checked state, renders nothing of its own
|_ <Control> β the interactive element: carries the switch role and checked state
| |_ <Thumb> β the knob; purely visual, styled off data-state
|_ <Label> β names the control; pressing it toggles
Using the switch is a walkthrough of intent, not a prop list:
- The root owns the checked state, exposed controlled and uncontrolled,
mirroring native patterns: an uncontrolled switch can be seeded checked,
while a controlled consumer drives it from outside β and every toggle intent
is reported back so the consumer stays in sync. Controlled is that sync
contract, not a hard gate: a toggle intent takes effect immediately and is
reported; the
checkedprop re-applies only when its value changes. - The control is the single interactive element. Pressing it toggles; it
carries the
switchrole and the checked state to assistive tech. - The thumb is purely visual β the knob consumers animate between the two ends of the track. It carries no behavior, only the styling hooks.
- The label names the control. Pressing it toggles too, matching native label ergonomics. The control's ARIA name follows what is actually rendered β an omitted Label never leaves a dangling reference.
- Disabled blocks toggling β from the control and the label alike β and is exposed to assistive tech and styling. It gates user intent only: a controlled or programmatic change still applies while disabled, so the consumer's state never desyncs.
| State | Behavior |
|---|---|
unchecked |
The setting is off. A toggle intent moves to checked unless disabled. |
checked |
The setting is on. A toggle intent moves to unchecked unless disabled. |
Disabled is a flag over both states, not a third state: the switch keeps its checked value while disabled and resumes toggling when re-enabled. The decision to block lives in the machine, so every substrate inherits it β a part's press binding never second-guesses it.
A Label can appear or disappear at any time β the ARIA name relationship on the Control always follows what is actually rendered.
Per APG Switch:
- Role: the control is
switch, witharia-checkedalways present and mirroring the state βtrueorfalse, never mixed (the switch is binary). - Name: the control is labelled by the rendered Label, or by an accessible label the consumer puts on the control in the no-label case. One of the two must be present.
- Keyboard: the control is focusable and Space toggles (Enter as well on a substrate whose native control activates on Enter). Substrates deliver this by rendering a natively activatable element β activation stays a single "press" intent.
- Disabled: exposed as
aria-disabled; every part also carriesdata-disabledfor styling. - State styling: every part carries
data-state="checked" | "unchecked"β the consumer's styling and animation hook.
- The control must always resolve an accessible name β from a rendered Label or a consumer-supplied label β never neither.
- ARIA labelled-by must only reference elements that are actually rendered.
aria-checkedalways mirrors the machine state; it is nevermixed.- Every checked β unchecked transition, whatever its cause, is reported to the consumer.
- While disabled, no user intent changes the state; controlled and programmatic changes still apply.
- Form integration (name/value, hidden input, form reset) is out of scope for v0 β the switch is a pure setting control.
- The Label is not a substrate-native
<label>. A native label forwards activation to its control, which would double-fire next to the part's own press-to-toggle binding β and not every substrate has a label element. The Label part instead carries its own press binding (toggling through the same machine intent, so the disabled guard applies) and the name flows througharia-labelledby. - Toggle is one event, gated in the machine. Control press, label press,
and keyboard activation all send the same
toggleintent; the disabled guard decides. Programmatic changes (check/uncheck) bypass the guard on purpose β see Disabled under States.