Buttons
Accessible button patterns — semantic buttons, toggle states, and icon buttons
Overview
Buttons are one of the most common interactive elements on the web. When implemented correctly, screen readers announce them as “button” with their label, and users can activate them with Enter or Space. When implemented with a <div>, none of this works.
WCAG Criteria:
- 2.1.1 Keyboard — all functionality must be operable via keyboard
- 4.1.2 Name, Role, Value — interactive elements must expose their role and name
Key requirements:
- Use the
<button>element (not<div>or<span>) - Ensure keyboard focusability and activation (Enter/Space)
- Provide a visible focus indicator
- For toggle buttons, use
aria-pressedto convey state - For icon-only buttons, use
aria-labelto provide an accessible name
Basic Button
Semantic Button vs. Div
View inaccessible code
<div class="btn" onclick="handleClick()">Submit</div>View accessible code
<button type="button" class="btn">Submit</button>What’s wrong with the div?
- No
buttonrole announced by screen readers — it’s just text - Not focusable with Tab key
- Cannot be activated with Enter or Space
- No focus indicator visible
onclickonly works with mouse
Predicted semantic summary (exact output varies):
| Version | Announcement |
|---|---|
Inaccessible (<div>) | “Submit” (just text, no role) |
Accessible (<button>) | “Submit, button” |
Toggle Button
Toggle Button with State
View inaccessible code
<!-- Label changes on toggle — confusing for SR 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 change implies it’s a different button
- With
aria-pressed, the label stays “Mute” and the state changes: “Mute, toggle button, not pressed” → “Mute, toggle button, pressed”
Icon Button
Icon Button with Accessible Name
View inaccessible code
<!-- No accessible name — SR says just "button" -->
<button>
<svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20">
<path d="M4 4L16 16M16 4L4 16" stroke="currentColor" stroke-width="2"/>
</svg>
</button>View accessible code
<!-- aria-label provides the accessible name -->
<button aria-label="Close dialog">
<svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20">
<path d="M4 4L16 16M16 4L4 16" stroke="currentColor" stroke-width="2"/>
</svg>
</button>Predicted semantic summary (exact output varies):
| Version | Announcement |
|---|---|
Without aria-label | ”button” (no name — user has no idea what it does) |
With aria-label | ”Close dialog, button” |
Use an ordinary action button when its label describes what will happen next, such as “Show password” changing to “Hide password.” Use aria-pressed only when a stable action represents an on/off state, such as “Mute” or “Bold.” A changing action label and aria-pressed together can create contradictory state information. For visible-text buttons, ensure the accessible name contains that visible text.