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
Inaccessible
<!-- 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>Live Preview
Accessible
<!-- 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>Live Preview
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
What the screen reader announces:
| 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
Inaccessible
<!-- Label changes on toggle — confusing for screen reader users -->
<button onclick="this.textContent =
this.textContent === 'Mute' ? 'Unmute' : 'Mute'">
Mute
</button>Live Preview
Accessible
<!-- 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>Live Preview
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
What the screen reader announces:
| 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.