Skip to Content
Component ExamplesButtons

Buttons

Accessible button patterns — semantic buttons, toggle states, and icon buttons

Maintained by Martin KrugerLast reviewed

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:

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-pressed to convey state
  • For icon-only buttons, use aria-label to provide an accessible name

Basic Button

Semantic Button vs. Div

Inaccessible
Submit
View inaccessible code
<div class="btn" onclick="handleClick()">Submit</div>
Accessible
View accessible code
<button type="button" class="btn">Submit</button>

What’s wrong with the div?

  • No button role announced by screen readers — it’s just text
  • Not focusable with Tab key
  • Cannot be activated with Enter or Space
  • No focus indicator visible
  • onclick only works with mouse

Predicted semantic summary (exact output varies):

VersionAnnouncement
Inaccessible (<div>)“Submit” (just text, no role)
Accessible (<button>)“Submit, button”

Toggle Button

Toggle Button with State

Inaccessible
View inaccessible code
<!-- Label changes on toggle — confusing for SR users --> <button onclick="this.textContent = this.textContent === 'Mute' ? 'Unmute' : 'Mute'"> Mute </button>
Accessible
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

Inaccessible
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>
Accessible
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):

VersionAnnouncement
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.


Resources