ARIA Roles and Attributes
ARIA adds semantic meaning to elements that HTML alone can't express — but the first rule of ARIA is to not use it if native HTML already does the job.
- Explain what ARIA is and what problem it solves
- State the first rule of ARIA and apply it to a design decision
- Use aria-label, aria-labelledby, and aria-describedby correctly
- Keep aria-expanded and aria-hidden in sync with visual state
- Explain what a live region is and when to use one
HTML elements carry built-in semantics — a <button> is announced as a button,
a <nav> is announced as a navigation landmark, an <input type="checkbox">
is announced with its checked state. But the web has components that HTML alone
cannot express: a custom tab panel, a combobox built from <div> elements,
a live notification region. This is the problem ARIA was designed to solve.
ARIA — Accessible Rich Internet Applications — is a set of HTML attributes, published by the W3C, that supplements the native semantics of HTML. It communicates three kinds of information to assistive technologies: roles (what something is), properties (what it does), and states (what condition it is currently in).
The first rule of ARIA
Before reaching for any ARIA attribute, apply this rule:
Do not use ARIA if a native HTML element or attribute already provides the semantics and behaviour you need.
This is not unofficial advice — it is the opening line of the W3C's ARIA
Authoring Practices Guide. A <button> is always preferable to
<div role="button">. A <label> is always preferable to aria-label when
visible text is available. Native elements are focusable, keyboard-operable,
and announce their semantics correctly out of the box. ARIA only provides the
announcement — it does not add focus behaviour or keyboard event handling.
When the native element exists, use it. ARIA fills the gaps.
Roles
A role attribute overrides or supplements the element's native role in the
accessibility tree. Some roles you will encounter:
<!-- A non-native dialog — no <dialog> element used -->
<div role="dialog" aria-labelledby="dialog-title" aria-modal="true">
<h2 id="dialog-title">Confirm deletion</h2>
…
</div>
<!-- A status message announced without user navigation -->
<div role="alert">Your changes have been saved.</div>
<!-- A tab strip built from custom elements -->
<div role="tablist">
<button role="tab" aria-selected="true" aria-controls="panel-1">Overview</button>
<button role="tab" aria-selected="false" aria-controls="panel-2">Details</button>
</div>
<div role="tabpanel" id="panel-1">…</div>
<div role="tabpanel" id="panel-2" hidden>…</div>The native <dialog> element now has broad browser support and carries
role="dialog" implicitly. Prefer it over a <div role="dialog"> — it
handles focus management and the Escape key for free.
Properties: labelling and describing
Properties are stable facts about an element. The three you will use most:
aria-label — provides an accessible name when there is no visible label
text. Use it on icon-only buttons:
<!-- The "X" is visual shorthand — the accessible name should be descriptive -->
<button aria-label="Close dialog">✕</button>aria-labelledby — points to an existing element whose text becomes the
accessible name. Preferred over aria-label when a visible label already
exists, because it reuses real content:
<h2 id="billing-heading">Billing address</h2>
<form aria-labelledby="billing-heading">
…
</form>aria-describedby — points to an element that provides supplementary
description, announced after the primary label. Used for hint text, error
messages, or additional context:
<input type="email" id="email" aria-describedby="email-hint" />
<p id="email-hint">We will send your receipt to this address.</p>aria-label wins over any visible text if both are present — which can create
a mismatch between what sighted users read and what screen reader users hear.
Prefer aria-labelledby to reference visible text, reserving aria-label
for elements that genuinely have no visible label (icon buttons, landmark
regions on pages with multiple of the same type).
States: keeping ARIA in sync
States reflect the current condition of an element and change as the user
interacts with the page. The critical rule: ARIA states must always match
the visual state. If the disclosure widget is open, aria-expanded must be
"true". If it is closed, aria-expanded must be "false". A state that
lags behind the visual creates a worse experience than no ARIA at all.
Common states and their usage:
<!-- A disclosure button — toggled by JavaScript -->
<button aria-expanded="false" aria-controls="menu">Menu</button>
<ul id="menu" hidden>…</ul>// When the button is clicked, update both the visual state and the ARIA state
button.addEventListener('click', () => {
const isOpen = button.getAttribute('aria-expanded') === 'true';
button.setAttribute('aria-expanded', String(!isOpen));
menu.hidden = isOpen;
});Other states you will encounter:
| Attribute | Values | Use case |
|---|---|---|
aria-checked | "true", "false", "mixed" | Custom checkboxes and radio buttons |
aria-disabled | "true", "false" | Elements that look disabled but remain focusable |
aria-hidden | "true" | Remove decorative content from the accessibility tree |
aria-invalid | "true", "grammar", "spelling" | Form validation errors |
aria-hidden: a sharp tool
aria-hidden="true" removes an element and all its descendants from the
accessibility tree — screen readers will not announce them. This is useful
for decorative icons:
<!-- The SVG is decorative; the visible text label is sufficient -->
<button>
<svg aria-hidden="true" focusable="false">…</svg>
Save
</button>The trap: never apply aria-hidden="true" to a focusable element. If a
user can Tab to it, the screen reader will announce it regardless of
aria-hidden — and the resulting announcement will be empty or confusing. If
you want to hide something from the tab order too, also set tabindex="-1" and
remove any interactive children from focus.
Live regions
A live region is an element the browser monitors for changes and automatically announces to screen readers. This is how dynamic content — search results loading, error messages appearing, a shopping cart updating — gets communicated without the user navigating to it:
<!-- polite: announced after the current task finishes -->
<div aria-live="polite" aria-atomic="true">
<p id="status"></p>
</div>
<!-- assertive: interrupts immediately — reserve for critical errors only -->
<div role="alert"><!-- content injected by JS here --></div>// Injecting content into a live region triggers the announcement
document.getElementById('status').textContent = '3 results found.';Use aria-live="polite" for non-urgent updates. Use role="alert" (which
implies aria-live="assertive") only for errors or critical messages — it
interrupts whatever the user is doing.
Where to go next
You can now communicate role, state, and properties to assistive technologies. Next: Color and Contrast — the mathematical standard for ensuring text remains readable for users with low vision.
Keyboard Navigation
Every interactive element must be reachable and operable with only a keyboard — and focus must always be visible.
Color and Contrast
Color contrast is a mathematical ratio between foreground and background — WCAG sets minimum ratios that ensure text is readable for users with low vision.