Dev view

Switch

A binary on/off control with immediate effect — track plus thumb that slides between two positions. Uses `role="switch"` to signal "this control immediately enables/disables a behaviour" rather than "this is a boolean form field" (which is `Checkbox`). Common alternate name "Toggle" across design systems (Atlassian, Carbon, Polaris). No tri-state mixed value — switches are strictly binary.

Also called Toggle Toggle switch On/off switch

When to use

Use

For immediate-effect on/off controls — settings panes that take effect on activation, feature flags, dark-mode toggle, notification opt-in / opt-out, sound mute. Pair with a noun-shaped label ("Notifications", "Dark mode", "Save drafts") that stays constant across both states; the on/off semantic is conveyed by the switch position. Use when activation should immediately apply without form submission.

Avoid

For boolean form fields that submit with the form — that is `Checkbox`. The semantic differs: switches signal "this setting is now active"; checkboxes signal "this field has this value when the form submits". For mutually-exclusive multi-option choice (List / Grid view) — that is `SegmentedControl`. For toggle buttons in a toolbar (bold / italic / underline) — those are `Button` with `aria-pressed`, not Switch. Never use a switch when the activation needs explicit confirm-then-apply (deferred- effect contexts where a partial choice should not yet take effect).

Versus related

  • checkbox

    `Checkbox` is a boolean form field whose value submits with the form (only checked checkboxes submit); `Switch` is an immediate-effect control whose activation applies the behaviour right away. Checkbox supports the tri- state mixed value for parent/child propagation; Switch is strictly binary. Decision test: does the activation submit a form value (Checkbox) or immediately apply a setting (Switch)?

  • segmented-control

    `SegmentedControl` selects one of multiple mutually- exclusive options (List / Grid / Calendar); `Switch` is a binary on/off. SegmentedControl is multi-option mutex; Switch is binary. Use SegmentedControl when the choice has more than two values; Switch only when the answer is exactly on or off.

  • button

    A toggle `Button` with `aria-pressed` is a momentary action that may be in a pressed state (toolbar formatting buttons — bold / italic). `Switch` is a setting-state control that persists. Decision test: does activation perform a transient action with state memory (toggle button) or change a configuration setting that persists (Switch)?

Switch is the canonical immediate-effect-on-off primitive. Track with sliding thumb communicates "this control toggles a behaviour the moment you activate it" — settings panes, feature flags, notification preferences, dark-mode toggles. Distinct from Checkbox by the activation semantic (Switch is "do this now", Checkbox is "this field carries this value when the form submits") and by the absence of the indeterminate (mixed) state. The reference documents the four canonical slots, the noun-not- verb label rule, the form-integration bridge (no native HTML input exists for switch — components must mirror state into a hidden `<input type="checkbox">` or controlled value), and the divergence from Checkbox and SegmentedControl.

Highlight
Fig 1.1 · Switch · Dev view
Dev

Code anatomy

Slot Code slot Semantic
root root label-or-div
track track presentational
thumb thumb presentational
input input input-or-button-with-role-switch
label label label-text
description description span-or-div
Both

Variants, properties, states

Variants

Structurally different versions of the component.

standard with-icons

Properties

The same component, parameterised.

PropertyType
size sm | md
disabled boolean
required boolean

States

Browser/user-driven (interactive) vs. app-driven (data).

KindStates
interactive
hoverfocus-visibleactivedisabled
data
offon
Both

State transitions

FromToTrigger
offonUser activates the input (Space, Enter, or click on label)
onoffUser activates the input (Space, Enter, or click on label)
Dev

Cross-framework expression

FrameworkStructure mechanismVariant mechanism
Web Components A `<ui-switch>` host with `name`, `value`, `checked`, `disabled`, `required` attributes; renders track + thumb + hidden `<input type="checkbox" role="switch">` for form participation attributes (`size="md"`, `disabled`, `with-icons`); CSS `:state(checked)` for state-driven styling; reflects to hidden input via element-internals or the bridge input
React Function component (Radix Switch.Root + Thumb, React Aria Switch, Material UI Switch, Mantine Switch); compound or prop-driven; controlled via `checked` (or `isSelected`) and `onCheckedChange`; hidden native input rendered when `name` is passed props with class-variance-authority; `size` / `disabled` / `required` props; `checked` is strictly boolean (no "indeterminate" allowed unlike Checkbox)
Angular (signals) Angular component with input<boolean>('checked'), input<boolean>('disabled'); ControlValueAccessor for ngModel/formControl integration; renders hidden input under formAssociated host input<'sm' | 'md'>('size'); host-binding [attr.role]="'switch'" plus [attr.aria-checked]; signal- derived state from controlled prop
Vue Single-file component with v-model for checked state (boolean); PrimeVue InputSwitch, Naive UI Switch as third-party precedents defineProps with literal-union types; computed property for aria-checked from checked; emit('update:modelValue') for controlled binding
Both

Events

  1. checkedChange
    Payload
    `{ checked: boolean, previousChecked: boolean }`. Fires when the switch toggles via user interaction (Space, Enter, or click on label). Strictly binary — `checked` is `true` or `false`, never `'mixed'`.
    Web Components
    `checkedChange` CustomEvent on the host with `event.detail = { checked, previousChecked }`.
    React
    `onCheckedChange(checked)` callback (Radix idiom) or `onChange(isSelected)` (React Aria idiom).
    Angular Signals
    `output<boolean>('checkedChange')` plus ControlValueAccessor `onChange(value)` for forms.
    Vue
    `@update:modelValue` event for v-model binding; `@change` for non-controlled usage.
Dev

Form integration

HTML5 validation
For form-bound switches, the native Constraint Validation API surfaces `valueMissing` when `required` is set and the switch is off. Most switches are immediate-effect (not form-bound) so validation does not apply. When a switch is conceptually required ("must accept terms" — though `Checkbox` is more idiomatic there), use the bridge plus `required` attribute.
Both

Accessibility

Slot Accessibility hint
root When the root is `<label>`, the input becomes the accessible name's host automatically. When the root is `<div>`, the inner `<label for="<input-id>">` provides association. Hit-target ≥ 24×24 px (WCAG 2.5.8); the wrapping label region is the canonical clickable area — clicking anywhere on the label toggles the switch.
track Decorative — `aria-hidden="true"` (or rendered via SVG without a `role`). The state is announced via the input's `aria-checked` value; SR users hear "switch, on" / "switch, off", not "track at right position". Track colour is reinforcement; the thumb position is the primary state signal.
thumb Decorative — `aria-hidden="true"`. Thumb position communicates state to sighted users; AT receives state via `aria-checked` on the input host. The thumb may carry an optional inline icon (check for on, X for off — Material 3 precedent) but the icon is also decorative.
input `role="switch"` is mandatory — a `<button>` without the role is announced as "button" not "switch". Native HTML has no `<input type="switch">` (the experimental `switch` attribute is limited browser support); the canonical pattern is `<input type="checkbox" role="switch">` for form integration or `<button role="switch" aria-checked="true|false">` for custom-rendered switches. `aria-checked="mixed"` is INVALID for switches — the role only accepts true / false per APG.
label Label text is the input's accessible name. SR users hear "switch, Notifications, on" / "switch, Notifications, off" — the noun stays constant; the state is carried by the role plus `aria-checked`. A label like "On" / "Off" or "Enabled" / "Disabled" violates the APG rule; the user cannot identify what is being toggled.
description Associate via `aria-describedby` on the input. SR announces description after the label and the on/off state. Avoid using description for the on/off semantic — that lives in the role plus `aria-checked`.
Both

Accessibility acceptance

Keyboard walk

KeysExpected
TabFocus moves to the input. Focus ring renders on the track or wrapping label per design system convention. Switch is its own tab stop.
Space (input focused)Toggles the state. Off → on, on → off. `aria-checked` flips and the thumb translates. Per APG, Space is canonical; Enter is optional (most libraries accept both for symmetry with other interactive controls).
Enter (input focused)Optional toggle per APG. Most implementations bind Enter to toggle for symmetry; some restrict to Space only. Document the choice.

Screen-reader announcements

TriggerExpected
SR encounters an off switch"Switch, [label text], off". The role, accessible name, and state are all announced. Some SR announce "off" as "not pressed" or "unchecked" depending on the SR's switch-handling — the role announces as "switch" when supported.
SR encounters an on switch"Switch, [label text], on".
User toggles the switchSR announces the new state — "on" / "off". The label text does NOT announce again because it has not changed (per APG constant-label rule).

axe-core rules to assert

  • aria-allowed-attr
  • aria-required-attr
  • aria-valid-attr-value
  • label
  • color-contrast

Same data as JSON for direct ingestion into Playwright + @axe-core/playwright or Jest + jest-axe: /api/components/switch/a11y-fixture.json

Both

Contracts

Non-negotiable contracts

  1. APGAPG: Switch pattern

    The label is a noun describing the controlled behaviour and stays constant across both states. Implementations that ship "On" / "Off" or "Enabled" / "Disabled" labels violate the APG constant-label rule. The on/off semantic is conveyed by the track-and- thumb visual plus the input's `role="switch"` and `aria-checked`.

    Without the constant noun label, users cannot identify what the switch controls. SR users hear "switch, On, on" — circular and meaningless. The label is the canonical accessible name; state words duplicate the role + `aria-checked` announcement and break the identification contract.

  2. APGAPG: Switch role binary states

    Switch is strictly binary — `aria-checked` accepts only `true` and `false`. The mixed (indeterminate) value is INVALID for `role="switch"` per APG. Tri- state semantics belong to `Checkbox`.

    Without this rule, implementations ship third "indeterminate" or "mixed" visuals that violate the ARIA spec. AT receives invalid attribute values and announces unpredictably (some SR fall back to "checked" silently; others read "mixed" but the role does not support it). The misuse is a category error — Switch is not Checkbox.

  3. HTML specHTML form-submission via input checkbox

    Form-bound switches render a hidden `<input type="checkbox" role="switch">` (or controlled-state bridge to a native checkbox) so form submission carries the value. Implementations using `<button role="switch">` without a form bridge lose state silently on submit.

    Without the bridge, switches inside `<form>` produce no submission data. Users save settings that do not persist; the bug is silent and only surfaces in downstream behaviour ("my preferences keep resetting"). Native HTML has no `<input type="switch">` so the bridge is the canonical pattern.

Vocabulary drift

WAI-ARIA
`role="switch"` (with `aria-checked` true | false)
WAI-ARIA switch role. Native HTML has no equivalent input type — the experimental `switch` attribute on checkbox has limited browser support. The canonical web wire is `<input type="checkbox" role="switch">`.
HTML
`<input type="checkbox" switch>` (experimental)
Experimental `switch` attribute (Safari 17.4+, partial Chrome support). Renders as a switch visual without custom CSS. Limited browser support; the role-override bridge remains canonical until adoption catches up.
Material 3
Switch
Material 3 codifies the with-icons variant — a check glyph on the thumb when on, optional dash when off — as a first-class visual treatment. Canonical name match; the icon variant is one of the canonical variants in the UI Anatomy reference.
Atlassian
Toggle
Atlassian uses "Toggle" — naming-only divergence from the canonical "Switch". Same role and contracts.
Carbon
Toggle
Carbon uses "Toggle" with on/off icons baked into the track. Canonical contract is preserved; naming and with-icons variant align with Material 3.
Polaris
Setting Toggle
Polaris combines a Switch with a heading-and-helper- text wrapper as a "Setting Toggle". The wrapper is composition; the switch primitive itself follows the canonical Switch contract.
Apple HIG
Toggle
Apple HIG distinguishes immediate-effect from deferred- effect toggles. iOS UISwitch and SwiftUI Toggle map to the immediate-effect canonical pattern. Canonical name divergence ("Toggle"); the immediate-effect semantic is the canonical one for the Switch component.
Dev

Common mistakes

Blocker

#switch-label-is-state

Label text is the on/off state instead of the controlled noun

Problem

The visible label reads "On" / "Off", "Enabled" / "Disabled", or "Yes" / "No". Users cannot identify what the switch controls; SR users hear "switch, On, on" — circular and meaningless. The APG rule that the label must stay constant is violated when the label text itself shifts with state.

Fix

Use a noun describing the controlled behaviour ("Notifications", "Dark mode", "Save drafts"). The label stays constant; on/off is conveyed by the track- and-thumb visual plus the input's `aria-checked`. SR users hear "switch, Notifications, on" — clear and identifiable.

Blocker

#switch-no-form-bridge

Form-bound switch loses state on submit because no hidden input is rendered

Problem

The switch is rendered as `<button role="switch">` and placed inside a `<form>`. The form submits but the switch's state is not in the form data — `<button>` does not contribute form values. Users save settings that do not persist; the bug is silent (no error, no visual cue).

Fix

Render a hidden `<input type="checkbox" role="switch" name="<key>" value="<on-value>">` (the native checkbox provides form participation; `role="switch"` overrides the announcement) or mirror the switch state into a hidden checkbox via JS bridge. Native form submission then carries the value. Immediate-effect switches that do not bind to a form omit the bridge and apply the change directly via state-update.

Major

#switch-color-only-state

Switch state communicated via track colour alone

Problem

The off and on states differ only in track-background colour (e.g. light grey ↔ accent blue). Colour-blind users and users in grayscale or low-light displays cannot distinguish the states. Fails WCAG 1.4.1 (Use of Colour).

Fix

The thumb position is the primary state signal — off sits at the inline-start, on sits at the inline-end. Track colour is reinforcement, not the sole signal. Test in grayscale (`filter: grayscale(1)`) — the thumb position must remain unambiguous. Optional with-icons variant adds a check / dash glyph to the thumb for additional reinforcement.

Major

#switch-aria-checked-mixed

Switch uses `aria-checked="mixed"` for an indeterminate visual

Problem

The switch ships a third "indeterminate" or "mixed" visual for when the controlled state is loading or partially applied. The implementer sets `aria-checked="mixed"`. Per APG, `mixed` is invalid for `role="switch"` — the role accepts only `true` and `false`. AT users hear unexpected announcements; some SR fall back to "checked" silently.

Fix

Switch is strictly binary. For loading / pending states, pair the switch with a separate progress indicator or use the disabled state during the transient phase. For tri-state semantics, the canonical surface is Checkbox (which validly supports `aria-checked="mixed"`), not Switch.

Major

#switch-target-too-small

Hit-target is below the WCAG 2.5.8 24×24 threshold

Problem

The clickable area (track plus thumb plus surrounding label region) is below 24×24 CSS pixels — common in compact settings rows where the track is rendered at 28×16 or smaller. Mobile users and motor-control- limited users struggle to activate; the visual track is misleading.

Fix

Extend the hit-target via the wrapping `<label>`. The track may stay 32×18 or 36×20 visually, but the clickable label region (track + label + adjacent padding) is ≥ 24×24. Verify with browser DevTools or automated hit-target audits. Per Apple HIG iOS, the threshold is 44×44 pt; meet the higher bar on touch-first surfaces.

Figma↔Code mismatches
  1. 01
    Figma

    Track colour drawn as the only state signal (grey → blue)

    Code

    Thumb position is the primary state signal; track colour is reinforcement

    Consequence

    Designers ship a switch where the only visual change between off and on is the track-background colour; developers ship a switch where the thumb also slides from inline-start to inline-end. The two surfaces visually agree in colour but designers may not draw the thumb-translation path, leaving developers without a motion specification.

    Correct

    Document the thumb position as the primary state signal in the anatomy. Figma carries an off-state frame (thumb at inline-start) and an on-state frame (thumb at inline-end). Code uses `transform: translateX(...)` on state change; the track-colour shift is secondary.

  2. 02
    Figma

    Switch labelled "On / Off" or "Yes / No"

    Code

    Label is a noun ("Notifications") that stays constant; on/off carried by `aria-checked`

    Consequence

    Designers label the switch with the state words ("On" / "Off" or "Enabled" / "Disabled"); developers know the APG rule says the label must stay constant. The two surfaces ship contradictory labelling intent — designers may not realise the AT contract is violated when the visible label changes per state.

    Correct

    Document the label as a noun in the anatomy. Figma's label slot carries the canonical noun ("Notifications"); the on/off semantic lives in the track-and-thumb visual plus the input's `role="switch"` and `aria-checked`. Per-state label-text override is an anti-pattern.

  3. 03
    Figma

    Switch component variants for "checked"/"unchecked"/"indeterminate"

    Code

    Switch has only on / off; mixed (indeterminate) is INVALID per APG

    Consequence

    Designers ship three component variants by analogy with Checkbox; developers know `aria-checked="mixed"` is not a valid value for `role="switch"`. The third variant ships as visual-only with no semantic mapping — implementers either add a fake mixed visual or drop it, leading to design / implementation drift.

    Correct

    Switch is strictly binary. Document the two data states (off / on) and reject the mixed variant as a Switch anti-pattern in the anatomy. If the use case truly needs three states, the surface is a Checkbox or a RadioGroup, not a Switch.

  4. 04
    Figma

    Switch shipped without a hidden form-bridge input

    Code

    Native HTML has no `<input type="switch">`; bridge required for form participation

    Consequence

    Designers do not consider form submission; developers know that `<button role="switch">` does not submit any value to the form. Without a hidden `<input type="checkbox" role="switch">` mirror or a controlled bridge in JS, the switch's state is lost on form submission. Settings panes that "save on submit" lose every switch value silently.

    Correct

    Document the form-bridge as canonical when the switch is form-bound. Either render `<input type="checkbox" role="switch">` (the native checkbox provides form participation, the role overrides the announcement) or mirror state into a hidden checkbox via JS plus controlled-prop bridge. Settings that take effect immediately without form submission omit the bridge.