Components · Forms & selection
Switch
A boolean form control for on/off state. The track is muted when off and Primary when on; 8px of padding expands the hit area toward 44px. Build it on a real checkbox for keyboard support.
Preview
Small — off
Small — on
Medium — off
Medium — on
Large — off
Large — on
Build with the skill
No package to install — hand this to your AI to generate or migrate the component in your stack.
Apply the Zetta "Switch" component to this project using the zetta-design-md skill.
WHEN TO USE
- Migrating or fixing existing code (the common case): audit the current implementation against the `switch` spec (variants: on, off, disabled) and CORRECT every deviation — replace off-spec values, don't only add new ones.
- New component: build it to the `switch` spec in this project's stack.
AUDIT & FIX (when the component already exists)
Compare it to the skill's spec and fix, don't just append: hardcoded colors/hex, off-scale radius, wrong fonts, dark/navy fills, shadows used in place of borders (or missing borders), brand lime used as a CTA or interaction (must be primary indigo), and any missing or wrong hover / focus / active / disabled / invalid state or missing 2px focus ring. Replace each with the correct Zetta token.
ALWAYS
- Pull the authoritative spec and tokens from the skill — never guess or hardcode values.
- Use this project's own component conventions and framework idioms.
- Honor the Zetta guardrails (brand = identity only, primary = interaction, shadows for overlays) and verify in Light, Dark, and Accessibility. Anatomy & rules
OFF state:
╭────────────────────╮
│ ╭──────╮ │ ← track: {colors.surface-muted} grey
│ │ ✕ │ │ ← thumb: white {colors.canvas}
│ ╰──────╯ │ icon: close, {colors.ink} dark on white
╰────────────────────╯
ON state:
╭────────────────────╮
│ ╭──────╮ │ ← track: {colors.primary} filled
│ │ ✓ │ │ ← thumb: white {colors.canvas}
│ ╰──────╯ │ icon: check, {colors.primary} — matches track
╰────────────────────╯
Thumb is always white ({colors.canvas}). Icons sit centered inside it:
- Off →
closeicon,{colors.ink}— dark X clearly visible on white - On →
checkicon,{colors.primary}— check is the same color as the track, creating a cohesive "confirmed" read - Icon size scales per size variant (
thumbIconSize) - Disabled → both icons use
{colors.disabled-text}regardless of on/off state
| State | Track | Thumb | Icon |
|---|---|---|---|
| Off | {colors.surface-muted} grey |
white {colors.canvas} |
close · {colors.ink} dark |
| On | {colors.primary} filled |
white {colors.canvas} |
check · {colors.primary} same as track |
| Disabled off | {colors.disabled-bg} |
white | close · {colors.disabled-text} |
| Disabled on | {colors.disabled-border} |
white | check · {colors.disabled-text} |
| Loading | 70% opacity on track | — | unchanged icon |
| Focus | — | — | 2px {colors.border-focus} outline at 2px offset |
When to use
- Settings and preferences where the user toggles a feature on or off.
- Binary configuration options that take effect immediately on change (or after Save in a form).
When not to use
- Toolbar actions (bold, italic, view mode) — use the Toggle button (
zetta-toggle-v1_6.md). - Mutually exclusive choices (A or B, not just on/off) — use Radio.
- Bulk row state in a table — use Checkbox.
- Dangerous or irreversible actions — use a Button + Dialog confirmation instead.
Do
- Always pair a Switch with a visible label. The label describes the feature, not the state.
- Use
switch-mdby default. Reserveswitch-smfor dense panels only. - Show a loading state (
switch-loading) when the toggle triggers an async operation.
Don't
- Don't rely on track color alone to communicate state — thumb position also carries the meaning.
- Don't use the label to describe the on/off state ("Enable notifications"). Label the feature ("Email notifications") — the switch communicates enabled/disabled.
- Don't use
switch-lgfor more than one switch on the same page.
- Label: noun phrase describing the feature. "Email notifications", not "Enable email notifications".
- Label length: under 40 characters. Wrap if longer — never truncate.
- Helper text: optional. 1 sentence max. Describes the impact. "You'll receive an email when someone mentions you."
Keyboard
| Key | Behavior |
|---|---|
Tab |
Move focus to the switch |
Space |
Toggle between on and off states |
Accessibility
- ARIA role:
role="switch"witharia-checked="true/false". Neverrole="checkbox". - Label association: connect via
aria-labelledbypointing to the label element, oraria-labelif no visible label. - Thumb icon:
aria-hidden="true"on both the ✕ and ✓ icons — they are decorative. State is communicated viaaria-checked, not the icon. - Disabled:
aria-disabled="true"alongside thedisabledattribute. - Loading: add
aria-busy="true"and announce state change completion via a live region. - Touch target: all sizes include 8px
paddingXandpaddingYthat extend the target beyond the visual track — do not remove.
Spec
Values and token references resolved straight from the Zetta spec.
switch
- trackColor
- {colors.surface-muted}
- trackBorderRadius
- {rounded.full}
- thumbColor
- {colors.canvas}
- thumbBorderRadius
- {rounded.full}
- thumbShadow
- {elevation.shadow.raised}
- cursor
- pointer
- paddingX
- {spacing.sm}
- paddingY
- {spacing.sm}
switch-sm
- trackWidth
- 32px
- trackHeight
- 18px
- thumbSize
- 14px
- thumbOffsetActive
- 16px
- thumbIconSize
- 8px
switch-md
- trackWidth
- 40px
- trackHeight
- 22px
- thumbSize
- 18px
- thumbOffsetActive
- 20px
- thumbIconSize
- 10px
switch-lg
- trackWidth
- 52px
- trackHeight
- 28px
- thumbSize
- 22px
- thumbOffsetActive
- 26px
- thumbIconSize
- 12px
switch-thumb-icon-off
- icon
- close
- iconColor
- {colors.ink}
- display
- flex
- alignItems
- center
- justifyContent
- center
switch-thumb-icon-on
- icon
- check
- iconColor
- {colors.primary}
- display
- flex
- alignItems
- center
- justifyContent
- center
switch-thumb-icon-disabled
- iconColor
- {colors.disabled-text}
switch-on
- trackColor
- {colors.primary}
switch-focus
- outline
- 2px solid {colors.border-focus}
- outlineOffset
- 2px
switch-disabled
- trackColor
- {colors.disabled-bg}
- thumbColor
- {colors.canvas}
- cursor
- not-allowed
switch-on-disabled
- trackColor
- {colors.disabled-border}
switch-loading
- cursor
- wait
- opacity
- 0.7
switch-label
- fontFamily
- Geist
- fontSize
- 14px
- fontWeight
- 400
- lineHeight
- 1.40
- textColor
- {colors.ink}
- cursor
- pointer
- userSelect
- none
switch-label-disabled
- textColor
- {colors.disabled-text}
- cursor
- not-allowed
switch-helper
- fontFamily
- Geist
- fontSize
- 12px
- fontWeight
- 400
- textColor
- {colors.muted}
- marginTop
- 2px
switch-wrapper
- display
- flex
- alignItems
- center
- gap
- {spacing.sm}