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
Inaccessible
<div class="btn" onclick="handleClick()">Submit</div>Live Preview
Submit
Accessible
<button type="button" class="btn">Submit</button>Live Preview
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
What the screen reader announces:
| Version | Announcement |
|---|---|
Inaccessible (<div>) | “Submit” (just text, no role) |
Accessible (<button>) | “Submit, button” |
Toggle Button
Toggle Button with State
Inaccessible
<!-- Label changes on toggle — confusing for SR 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 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
Inaccessible
<!-- 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>Live Preview
Accessible
<!-- 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>Live Preview
What the screen reader announces:
| Version | Announcement |
|---|---|
Without aria-label | ”button” (no name — user has no idea what it does) |
With aria-label | ”Close dialog, button” |