Switch
An on/off toggle. Built on native <input type="checkbox">. The
native input is visually hidden (but accessible) while a CSS-drawn
track and thumb provide the switch appearance. CSS-only.
When to use
Use a switch when a setting takes immediate effect — turning on notifications, enabling dark mode, etc. The change applies as the switch flips, with no "Save" step.
If the setting needs explicit confirmation, use a Checkbox with a
form action.
Variants
| Variant | Class | Use for |
|---|---|---|
| Default | .switch |
Standard accent fill when on |
| Success | .switch.-success |
Positive-state toggle (active / safe) |
| Danger | .switch.-danger |
Risky state (mute / unsafe enable) |
| Small | .switch.-s |
Compact size for dense UIs |
Anatomy
<label class="switch">
<input type="checkbox" class="switch__input" />
<span class="switch__track"></span>
<span class="switch__label">Enable notifications</span>
</label>
<!-- Pre-checked -->
<label class="switch -success">
<input type="checkbox" class="switch__input" checked />
<span class="switch__track"></span>
<span class="switch__label">Auto-save</span>
</label>
<!-- Disabled -->
<label class="switch">
<input type="checkbox" class="switch__input" disabled />
<span class="switch__track"></span>
<span class="switch__label">Coming soon</span>
</label>
The <label> wraps everything so the entire switch is clickable; the
<input> is visually hidden but accepts focus and keyboard input.
Tokens used
From semantic tier
--color-border-strong— track off--color-accent— track on, focus ring (default)--color-success— track on (-success)--color-danger— track on (-danger)--color-surface-canvas— thumb--color-ink-strong— label text--shadow-raised— thumb shadow--radius-pill— track radius--space-inset-element-s— thumb inset from track edge--space-gap-elements-m— gap between track and label--size-control-s/-m— track size base--border-width-hairline/--border-width-focus--font-body-regular-*— label typography--motion-fast/--motion-easing-default— thumb slide
Component-tier (defined inline)
--switch-track-w,--switch-track-h,--switch-thumb— overridable for custom sizing
Accessibility
The native <input type="checkbox"> carries all required semantics:
- Keyboard: Space toggles. Tab/Shift+Tab navigates.
- Screen readers announce "checkbox, checked/not checked" with the label.
- The
<label>wrapping provides the association — no explicitforattribute needed. - Focus appears on the track (via
:focus-visibleon the input + CSS adjacent selector) because the input itself is visually hidden.
The component uses the standard "visually hidden" pattern for the
input. This is the recommended approach over appearance: none +
custom styling because it preserves every accessibility behavior of
the native control.
Edge cases
- Form submission: the input has
nameandvalueattributes (set by author) so checked switches submit as form fields. Use<input class="switch__input" name="notifications" value="on">. - Indeterminate: native
<input type="checkbox">supportsindeterminatevia JS property. A switch typically does not need this state; if you do, use Checkbox instead. - Right-to-left scripts: the thumb position uses
inset-inline-startso it slides correctly in both LTR and RTL.
Do
- Wrap the input in
<label class="switch">. - Use Switch for instant settings; use Checkbox for forms with submit.
- Pair with a concise label that reads as a setting name, not a question ("Notifications", not "Do you want notifications?").
Don't
- Don't use
display: noneon the input. It removes accessibility. Use the visually-hidden pattern this component implements. - Don't put two switches on one row without clear separation. They are easily mistaken for siblings of one toggle.
- Don't bind a
clickhandler to the track. The native<input>fireschange— listen there.