Zetta v1.6

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.

Prompt for your AI
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:

  • Offclose icon, {colors.ink} — dark X clearly visible on white
  • Oncheck icon, {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-md by default. Reserve switch-sm for 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-lg for 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" with aria-checked="true/false". Never role="checkbox".
  • Label association: connect via aria-labelledby pointing to the label element, or aria-label if no visible label.
  • Thumb icon: aria-hidden="true" on both the ✕ and ✓ icons — they are decorative. State is communicated via aria-checked, not the icon.
  • Disabled: aria-disabled="true" alongside the disabled attribute.
  • Loading: add aria-busy="true" and announce state change completion via a live region.
  • Touch target: all sizes include 8px paddingX and paddingY that 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}