ARIA roles, states and live regions

HTML · Advanced · 12 min

Custom components like tabs, menus and toasts are only accessible if their roles and states are exposed.

ARIA (Accessible Rich Internet Applications) adds attributes that change what assistive technology is told about an element. It changes semantics only, never behaviour: role="button" doesn't make a div focusable or keyboard-operable.

That's why the first rule of ARIA is: don't use it if a native element does the job. A <button> beats <div role="button" tabindex="0"> plus key handlers every time.

When you do build custom widgets, ARIA comes in three kinds:

  • Roles: what something is, e.g. role="tablist", role="tab", role="tabpanel", role="alert".
  • Properties: relationships and names: aria-label, aria-labelledby, aria-describedby, aria-controls.
  • States: current status, which your JavaScript must keep updating: aria-expanded, aria-selected, aria-checked, aria-current="page", aria-invalid.

Live regions announce dynamic changes without moving focus. Put text into an element with aria-live="polite" (announced when the user is idle) or role="alert" (assertive, immediate) and screen readers read it. The region must exist in the DOM before you change its content.

aria-hidden="true" hides decorative content from assistive technology, but never put it on something focusable. The WAI-ARIA Authoring Practices Guide documents the expected roles and keyboard patterns for tabs, menus, comboboxes and more; follow it rather than inventing your own.

Example

<!-- Disclosure button: state kept in sync by script -->
<button type="button" aria-expanded="false" aria-controls="filters">Filters</button>
<div id="filters" hidden>...</div>

<!-- Tabs (keyboard handling with arrow keys still needed in JS) -->
<div role="tablist" aria-label="Account">
  <button role="tab" id="t1" aria-selected="true" aria-controls="p1">Profile</button>
  <button role="tab" id="t2" aria-selected="false" aria-controls="p2" tabindex="-1">Billing</button>
</div>
<div role="tabpanel" id="p1" aria-labelledby="t1">...</div>
<div role="tabpanel" id="p2" aria-labelledby="t2" hidden>...</div>

<!-- Live region: present on load, filled later -->
<div aria-live="polite" id="status"></div>
<script>
  document.getElementById("status").textContent = "3 results found";
</script>

Remember

  • No ARIA is better than bad ARIA: prefer native elements
  • ARIA changes semantics only, not behaviour
  • Keep states like aria-expanded in sync from JavaScript
  • Live regions must exist before their content changes

Watch out for

  • Adding role="button" to a div and forgetting tabindex and Enter/Space handling.

Check yourself

  1. What does adding role="button" to a <div> give you?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA

Builds on

Unlocks

Nothing builds on this yet.