Toggle Controls (Switch, Toggle Button, Checkbox)
Three patterns for binary state — when to use each, and how screen readers announce them differently.
Overview
Toggle controls let users switch between two states — on/off, mute/unmute, dark/light. There are three distinct patterns: switches (role="switch") for settings that take immediate effect, toggle buttons (aria-pressed) for actions that can be toggled, and checkboxes for form values submitted later. Each announces differently to screen readers, and choosing the wrong pattern confuses assistive technology users about what will happen when they interact.
WCAG Criteria:
- 4.1.2 Name, Role, Value — interactive elements must expose their role, name, and current state
- 1.3.1 Info and Relationships — information and relationships conveyed through presentation must be programmatically determinable
Key requirements:
- Use
role="switch"witharia-checkedfor settings that take immediate effect (e.g., dark mode, notifications) - Use
aria-pressedon a<button>for toggle actions (e.g., mute, bold, favorite) - Use
<input type="checkbox">for form values that are submitted later - The label NEVER changes when state changes —
aria-checkedoraria-pressedcommunicates the state - Space key toggles all three; switches and toggle buttons are single Tab stops
- Always provide visible focus indicators
Switch vs. Div Toggle
Switch vs. Div Toggle
View inaccessible code
<!-- Div toggle — no role, no state, no keyboard access -->
<div onclick="toggleSwitch()">
<div class="track"><div class="thumb"></div></div>
<span>Dark mode</span>
</div>View accessible code
<!-- Semantic switch — role, state, and keyboard built in -->
<button role="switch" aria-checked="false"
onclick="let on = this.getAttribute('aria-checked') === 'true';
this.setAttribute('aria-checked', String(!on))">
<span class="track"><span class="thumb"></span></span>
Dark mode
</button>What’s wrong with the div?
- No
switchrole announced — screen readers see only static text - No
aria-checkedstate — screen readers have no way to know if it’s on or off - Not focusable with the Tab key, so keyboard users cannot reach it
- Cannot be toggled with Space or Enter
- The visual animation is the only indication of state — invisible to assistive technology
onclickon a div only works with a mouse
Predicted semantic summary (exact output varies):
| Version | Announcement |
|---|---|
Inaccessible (<div>) | “Dark mode” (just text, no role, no state) |
Accessible (role="switch") | “Dark mode, switch, off” / “Dark mode, switch, on” |
Toggle Button
Toggle Button with aria-pressed
View inaccessible code
<!-- Label changes on toggle — confusing for screen reader users -->
<button onclick="this.textContent =
this.textContent === 'Mute' ? 'Unmute' : 'Mute'">
Mute
</button>View accessible code
<!-- Stable label + aria-pressed conveys state -->
<button aria-pressed="false"
onclick="let p = this.getAttribute('aria-pressed') === 'true';
this.setAttribute('aria-pressed', String(!p))">
Mute
</button>What’s wrong with changing the label?
- Screen reader users hear “Mute, button” then “Unmute, button” — the label flip implies it could be a completely different button
- There’s no programmatic state — the screen reader cannot tell the user whether mute is currently active or inactive
- With
aria-pressed, the label stays “Mute” and the state is clear: “not pressed” means mute is off, “pressed” means mute is on - Stable labels also help speech recognition users who need to say the button name to activate it — a changing name is a moving target
Predicted semantic summary (exact output varies):
| Version | Announcement |
|---|---|
| Label-swapping button | ”Mute, button” then “Unmute, button” (which is the current state?) |
Toggle button (aria-pressed) | “Mute, toggle button, not pressed” / “Mute, toggle button, pressed” |
When to Use Which Pattern
| Pattern | Element | State attribute | Use when |
|---|---|---|---|
| Switch | <button role="switch"> | aria-checked | Settings that take immediate effect (dark mode, notifications, Wi-Fi) |
| Toggle button | <button aria-pressed> | aria-pressed | Actions that can be toggled (mute, bold, favorite, pin) |
| Checkbox | <input type="checkbox"> | checked | Form values submitted later (agree to terms, opt-in preferences) |
Key differences:
- Switch = immediate effect, no submit step. Screen readers announce “switch, on/off.”
- Toggle button = an action with two states. Screen readers announce “toggle button, pressed/not pressed.”
- Checkbox = deferred submission as part of a form. Screen readers announce “checkbox, checked/not checked.”
- All three toggle with the Space key. Switches and toggle buttons also toggle with Enter.
- The label always stays the same — the state attribute alone communicates the change.
That stable-label rule applies to stateful toggles. It does not apply to every two-way action: a disclosure button can keep a stable label and expose aria-expanded, while a Show password action can legitimately change to Hide password without aria-pressed. Choose one coherent model based on whether the control represents persistent state or the next action.