Switch
components specs/components/switch.kmd
ON/OFF toggle with immediate effect. Material parity (`/components/switch`). Distinct from Checkbox — switch implies "this changes state right now"; checkbox implies "I'm submitting this value with the form."
When this spec applies
Primary triggers
- Implement immediate-effect toggle
All triggers
- Settings toggle that takes effect immediately
- Feature flag user can opt in/out
- Quick action (mute, mic, camera)
Specification body
Spec — Switch
Facet Visual do Koder Design. Material parity: https://m3.material.io/components/switch.
Anatomy
OFF: ●──── (handle left, track muted)
ON: ────● (handle right, track accent)
- Track: 52×32 px (default), radius-full pill
- Handle (thumb): 24 px circle, shifts left↔right
- Hit zone: 48×48 px
R1 — Switch vs Checkbox
Critical distinction:
| Use Switch when… | Use Checkbox when… |
|---|---|
| Action takes effect IMMEDIATELY | Action is part of a FORM submit |
| Single setting, no form context | Multiple options or terms acceptance |
| User can see the system change (WiFi, mute) | The change applies on Save |
| Quick toggle in toolbar | Per-row selection in a list |
Rule of thumb: if user must click a "Save" button after, use checkbox. If the change persists the moment they toggle, use switch.
R2 — States
| State | Track | Handle |
|---|---|---|
| OFF + enabled | surface-variant bg | surface circle |
| OFF + hover | text-muted 8% bg | + ring outline |
| OFF + focused | + focus ring | + focus ring |
| ON + enabled | accent bg | accent-on circle |
| ON + hover | accent-strong bg | + slight scale |
| ON + focused | + focus ring | + focus ring |
| Disabled | 38% opacity | 38% opacity |
R3 — Optional icons
Switch can show icons inside handle/track:
| Style | Visual |
|---|---|
| No icons (default) | Plain pill |
| Handle icon | Small icon inside handle (24 px) — check/x or feature glyph |
| Track icons | Icons at both ends of track (smaller, indicating state semantics) |
Optional; doesn't change behavior. Material 3 default includes check/x in handle for accessibility (visual state cue beyond color).
R4 — Animation
- Handle slides smoothly left↔right on toggle (motion-fast, ~150ms)
- Track color animates simultaneously
- Reduced-motion: instant (no slide)
- Sound (optional, app-specific): subtle click on toggle
R5 — Optimistic state
When toggling has async effect (server save), follow optimistic pattern:
- User toggles → switch UI immediately shifts to new state
- Async call kicks off in background
- If success: stay
- If failure: snap back to previous state + show error snackbar ("Couldn't enable notifications. Try again.")
NEVER show a spinner WITHIN the switch on async — it freezes the visual feedback.
R6 — Settings convention
In Settings pages, switch rows have this canonical layout:
┌────────────────────────────────────────────────────┐
│ [icon?] Title [●──] │
│ Optional description text │
└────────────────────────────────────────────────────┘
- Icon (leading, optional)
- Title (
title-medium) - Description (
body-smallmuted) below title - Switch right-aligned (or left in RTL)
- Whole row clickable
KoderSettingsTile.toggle() widget (per future
settings/patterns.kmd) — exposed by koder_kit.
R7 — Accessibility
<input type="checkbox" role="switch">(HTML standard, both attributes)- OR native Flutter
Switchwidget aria-checked="true"/"false"(set automatically with role=switch)- Visible focus ring
- Keyboard: Space toggles
- Screen reader announces: "Switch, Notifications, on" (or off)
- High-contrast: not just color — handle position is the primary cue
R8 — Forbidden patterns
- ❌ Switch for form submission semantics (use checkbox)
- ❌ Switch without label (always provide accessible name)
- ❌ Switch that requires confirm dialog after toggle (defeats the "immediate" semantic — use checkbox + button instead)
- ❌ More than ~10 switches in one page (becomes overwhelming; group into sub-sections)
- ❌ Tri-state switch (use radio group for >2 options)
R9 — Per-preset variation
| Preset | Visual |
|---|---|
material3 | Pill track + circular handle |
ios_cupertino | Same shape, slightly larger; uses iOS green for ON |
gnome | Less rounded pill (radius-md instead of radius-full) |
windows_11 | Mica backdrop track + handle |
brutalist | Sharp rectangle track + sharp square handle |
terminal_classic | ASCII [off] / [ON ] text |
Cross-link
components/checkbox.kmd— vs switch decisioninteraction/states.kmd— state visualsthemes/color-roles.kmd— accent track ONthemes/motion.kmd— slide animation
Requirements (testable)
Requirement: Toggle applies immediately {#req-switch-immediate-effect}
A Switch SHALL apply its state change the moment the user toggles it, with no intervening "Save" button or confirmation dialog; if a confirm step or deferred-submit semantic is required, a Checkbox SHALL be used instead (per R1 and R8).
Scenario: Toggling persists the change instantly
- GIVEN a Switch bound to an immediate-effect setting in the OFF state
- WHEN the user activates the Switch
- THEN the underlying setting SHALL transition to ON without requiring any further action
- AND no "Save" button SHALL be needed to commit the change
Scenario: No confirmation dialog after toggle
- GIVEN a Switch controlling a setting
- WHEN the user toggles the Switch
- THEN no confirmation dialog SHALL be presented after the toggle
- AND the new state SHALL take effect directly
Requirement: State reflects value via handle position and track color {#req-switch-state-visuals}
A Switch SHALL render its OFF state with the handle at the leading edge over a muted (surface-variant) track and its ON state with the handle at the trailing edge over an accent track; a disabled Switch SHALL render at 38% opacity (per R2).
Scenario: OFF state visuals
- GIVEN a Switch in the OFF and enabled state
- WHEN the Switch is rendered
- THEN the handle SHALL be positioned at the leading (left) edge
- AND the track SHALL use the
surface-variantbackground color
Scenario: ON state visuals
- GIVEN a Switch in the ON and enabled state
- WHEN the Switch is rendered
- THEN the handle SHALL be positioned at the trailing (right) edge
- AND the track SHALL use the
accentbackground color
Scenario: Disabled state dimming
- GIVEN a Switch in the disabled state
- WHEN the Switch is rendered
- THEN both the track and handle SHALL be drawn at 38% opacity
Requirement: Handle slides on toggle and respects reduced motion {#req-switch-animation}
A Switch SHALL animate the handle sliding between the leading and trailing edges while the track color animates simultaneously; when reduced-motion is active, the Switch SHALL snap instantly to the new state with no slide (per R4).
Scenario: Handle slides on normal toggle
- GIVEN a Switch in the OFF state with reduced-motion disabled
- WHEN the user toggles it to ON
- THEN the handle SHALL animate its position from the leading edge to the trailing edge
- AND the track color SHALL animate from the muted color to the accent color at the same time
Scenario: Reduced motion snaps instantly
- GIVEN a Switch in the OFF state with reduced-motion enabled
- WHEN the user toggles it to ON
- THEN the handle SHALL move to the trailing edge with no sliding animation
Requirement: Optimistic async state with rollback {#req-switch-optimistic-state}
When toggling triggers an asynchronous effect, a Switch SHALL update its visual state immediately (optimistic), and on failure SHALL snap back to the previous state and surface an error message; it SHALL NOT display a spinner within the Switch during the async operation (per R5).
Scenario: Failed async call rolls back and shows error
- GIVEN a Switch in the OFF state whose toggle triggers an async server save
- WHEN the user toggles it to ON and the async call fails
- THEN the Switch SHALL snap back to the OFF state
- AND an error message (snackbar) SHALL be shown to the user
Scenario: No in-switch spinner during async
- GIVEN a Switch whose toggle triggers a pending async call
- WHEN the async call is in progress
- THEN no spinner SHALL be rendered within the Switch
- AND the Switch SHALL remain showing the optimistic new state
Requirement: Accessible switch semantics and keyboard operation {#req-switch-accessibility}
A Switch SHALL expose switch semantics (role="switch" with aria-checked reflecting its value, or the native Switch widget), SHALL always carry an accessible name, SHALL toggle on the Space key, and SHALL show a visible focus ring (per R7 and R8).
Scenario: Space key toggles the switch
- GIVEN a Switch in the OFF state that has keyboard focus
- WHEN the user presses the Space key
- THEN the Switch SHALL toggle to the ON state
- AND
aria-checkedSHALL be set to"true"
Scenario: Switch exposes an accessible name and role
- GIVEN a rendered Switch labeled "Notifications"
- WHEN an assistive technology inspects the control
- THEN it SHALL report a switch role with the accessible name "Notifications"
- AND the current on/off state SHALL be conveyed via
aria-checked
Scenario: Focus shows a visible ring
- GIVEN a Switch that does not have focus
- WHEN the user moves keyboard focus to the Switch
- THEN a visible focus ring SHALL be rendered around the control
References
specs/interaction/states.kmdspecs/components/checkbox.kmdspecs/themes/color-roles.kmd