Skip to main content

Switch

betav0.1.0

Boolean on/off toggle built on @radix-ui/react-switch. Renders a native <button> (role="switch", aria-checked, Space/Enter keyboard activation for free) with an optional integrated label (Figma: label is part of the component, not a composed sibling — RFC arbitration Q4). Size and color are intentionally fixed: this version ships exactly the one size and one color the Figma spec defines (RFC arbitration Q1) — see Switch.rfc.md for the product evidence behind that exclusion.

Variants

Unchecked / checked (uncontrolled)

Default unchecked
Default checked

Disabled

Disabled unchecked
Disabled checked

With integrated label

Examples

<Switch aria-label="Enable notifications" />
<Switch label="Enable notifications" />
<Switch label="Auto-refresh" checked={enabled} onCheckedChange={setEnabled} />
<Switch label="Read-only mode" disabled />
<Switch ref={switchRef} label="Enable notifications" defaultChecked />

Props

PropType
checkedboolean — Controlled checked state
defaultCheckedboolean — Default checked state (uncontrolled)
onCheckedChange(checked: boolean) => void — Fires when the checked state changes
labelReactNode — Integrated label rendered next to the control (Figma: Q4 option a)
wrapperClassNamestring — Additional classes for the outer <label> wrapper (only applies when `label` is set)
disabledboolean — Disabled state
requiredboolean — Marks the control as required in a form
namestring — Name submitted with the owning form
valuestring — Value submitted with the owning form when checked (defaults to "on", like a native checkbox)
idstring — Overrides the auto-generated id used for label association
classNamestring — Additional classes for the control itself

Accessibility

2.1 AAWCAG fail

Native <button> (Radix Root) with role="switch"/aria-checked, native Space/Enter keyboard activation, controlled/uncontrolled value (checked/defaultChecked + onCheckedChange). forwardRef exposed on the underlying <button> (repo convention for form-field components). Label uses <label htmlFor>/<button id> native association. Focus ring uses the standard ring-focus/ring-offset-focus pattern (AGENTS.md 'Interactive Elements') — no deviation, no justification needed. KNOWN, ACCEPTED WCAG 1.4.11 FAILURE (1 pair below threshold) — shipped with the CURRENT Figma color tokens (2026-08-03 full re-capture + 2026-08-04 dedicated off-thumb token), allow-listed in scripts/wcag-allowlist.json (approved by Sandy, expires 2026-08-31 — pending design re-validation, see Switch.rfc.md); resolution deferred to a global color-token pass (not a Switch-local fix): track unchecked vs bg — dark 1.79:1 / light 1.60:1 (off track is --color-feedback-contrast-primary, white@20% translucent over the page; the track is the component's own body, so this stays gating). SUPPLEMENTAL, NOT A GATING FAILURE — thumb unchecked vs track unchecked: light 2.33:1 (dark 9.69:1). This band contrast is NOT the required 1.4.11 identifying pair here: the 20px thumb (size-5) OVERFLOWS the 14px pill (protrudes ±3px, implemented 2026-08-03), so its identifying adjacency is the PAGE BACKGROUND, not the internal track band — Understanding SC 1.4.11's Figure 23 (round toggle vs internal background) depicts a CONTAINED toggle and does not control an overflowing one. The light 2.33 (off thumb --color-feedback-neutral-off --gray-500 #7a7c85 over the light track #c2c2c2) is recorded in Switch.rfc.md §5.4 as supplemental evidence; the earlier --icon-default (fill-default) piggy-back only 'passed' it in light by wrongly resolving to near-black #18191b. The 2026-08-03 correction RESOLVED 3 #58 failures: track checked vs bg 3.31/3.14 (was 1.69 light); thumb checked vs track checked 3.11/3.57 (was 2.25 dark); thumb unchecked vs track unchecked dark 9.69. 2026-08-20 export (brand alpha 50% -> 55%) moves both brand-alpha pairs: track checked vs bg 3.31/3.14 -> 3.78/3.60 (the NORMATIVE pair, improves in both modes); thumb checked vs track checked 3.11/3.57 -> 2.73/3.11, so dark now sits under the 3 target. That pair is SUPPLEMENTAL (the 20px thumb overflows the 14px pill, so it is identified against the page background at 10.30/11.22, unchanged), and the mechanism is inherent: raising the alpha moves the track toward the brand hue the thumb itself uses. IDENTIFIABILITY IS NOT LOST: per Understanding SC 1.4.11 the thumb carries the state and stays ≥3:1 vs the page in BOTH modes (thumb checked 10.30/11.22; thumb unchecked 17.38/3.72). Disabled is exempt (WCAG 1.4.3 Note 1): disabled+checked thumb reuses --icon-highlight at opacity-40 (no Figma token for the ratio) and disabled+unchecked uses the distinct --icon-disabled variable (binding, not a tint). Hover halos are WCAG-exempt (SC 1.4.11 'Hover states': supplemental hover-only treatments need not meet 3:1; the thumb already carries the state) and declared only for Rule 5 coverage, not counted among the allow-listed failure: checked halo ring-filigran-brand-primary-transparency-10 (1.17/1.22); OFF halo ring-feedback-contrast-secondary (1.28/1.26, added 2026-08-03 — Figma 2686:5927 defines an off-value hover too, white@10%/black@10% mode-adaptive, correcting the older checked-only reading). ring-8 geometry is a bounding-box approximation (see Switch.figma.json). Hover only adds the halo; track tint + thumb fill are identical to Default, and the 20px thumb (size-5) overflowing the 14px h-3.5 pill inside the 20px Root frame is now IMPLEMENTED (2026-08-03, Switch.rfc.md §3.3). Size and color are excluded from this version (RFC arbitration Q1) — see Switch.rfc.md.