ARIA Done Right
ARIA (Accessible Rich Internet Applications) adds roles, states, and properties to the accessibility tree for custom widgets that HTML can’t express. But the first rule of ARIA is: don’t use ARIA — if a native element does the job, use it. ARIA changes only how AT announces an element; it adds no behavior, no keyboard handling, no focus. Bad ARIA is worse than none — it actively misleads screen-reader users.
What are the five rules of ARIA (the gist)?
- Don’t use ARIA if a native element/attribute exists with the semantics you need.
- Don’t change native semantics unless you must (
<button role="heading">— no). - All interactive ARIA controls must be keyboard-operable.
- Don’t put
role="presentation"/aria-hidden="true"on a focusable element (creates a focusable-but-invisible-to-AT trap). - All interactive elements need an accessible name.
Roles vs states vs properties?
- Role — what a thing is:
role="dialog",role="tab",role="alert". Set once. - State — current condition, changes at runtime:
aria-expanded,aria-checked,aria-selected,aria-disabled,aria-pressed. - Property — relatively static characteristics/relationships:
aria-label,aria-labelledby,aria-describedby,aria-controls,aria-haspopup.
The interview point: ARIA states must be kept in sync with JS — set aria-expanded="true" when you open the menu, "false" when you close it. Stale ARIA is a common bug.
aria-label vs aria-labelledby vs aria-describedby vs <label>?
| Mechanism | Use | Notes |
|---|---|---|
<label for> / wrapping |
the standard for form controls | preferred; clickable, native |
aria-labelledby="id" |
name from visible text already on the page | can reference multiple ids; wins over aria-label |
aria-label="text" |
name when there’s no visible text (icon button) | invisible string; easy to forget to localize |
aria-describedby="id" |
extra description (hint, error), not the name | announced after the name |
Accessible-name precedence: aria-labelledby > aria-label > native (<label>/alt/text content). Prefer visible text (labelledby) over invisible (label).
What are live regions?
Regions that announce dynamic content changes without moving focus — for toasts, validation summaries, async results:
<div aria-live="polite" aria-atomic="true">3 results found</div> <!-- waits for a pause -->
<div role="alert">Payment failed</div> <!-- assertive, interrupts -->aria-live="polite"— announce at the next opportunity (most cases).aria-live="assertive"/role="alert"— interrupt immediately (errors only).role="status"≈ polite;role="alert"≈ assertive. The live region must exist in the DOM before you update it, or some screen readers won’t announce the change.aria-atomic="true"reads the whole region, not just the diff.
What does aria-hidden="true" do, and the trap?
Removes an element (and its subtree) from the accessibility tree — it’s still visible and still in the layout. Use it to hide decorative duplicates (e.g., an icon next to visible text). Trap: never put it on or around a focusable element — keyboard users still reach it, but AT says nothing, so they’re “lost.” Hide visually and from AT together (move out of focus order too).
Give an example of ARIA fixing a real gap.
A custom toggle button reflecting on/off:
<button aria-pressed="false" id="mute">Mute</button>
<script>
mute.addEventListener("click", () => {
const on = mute.getAttribute("aria-pressed") === "true";
mute.setAttribute("aria-pressed", String(!on)); // keep state in sync
});
</script>Native <button> gives role + keyboard; aria-pressed adds the toggle state HTML has no element for.
Gotchas
- ARIA adds zero behavior —
role="button"on a<div>still needstabindex="0"+ Enter/Space handlers + focus styles. This is the most common misconception. - Stale states — forgetting to flip
aria-expanded/aria-selectedon interaction is a frequent defect; drive them from the same state as the visual. - Redundant roles —
<nav role="navigation">,<button role="button">are noise; native already has the role. - Over-labeling —
aria-labelon an element that already has visible text overrides it, sometimes hiding the visible label from AT. Don’t double-name. aria-liveadded at the same time as content may not announce — render the empty region first.titleattribute is not reliable for accessible names (inconsistent AT support, no touch/keyboard exposure) — use real labels.
Related
Further reading
- Using ARIA (W3C, incl. the rules of ARIA): https://www.w3.org/TR/using-aria/
- ARIA Authoring Practices Guide (APG): https://www.w3.org/WAI/ARIA/apg/
- MDN — ARIA live regions: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions
Interview angle 4
- “What is the first rule of ARIA?” - do not use ARIA. Use the element that
already has the semantics. ARIA changes what is announced and changes
nothing about behaviour, so
role="button"on a div still needstabindex, Enter and Space handlers, and a focus style - you have taken on all the work and only fixed the label. - “What are the three kinds of ARIA attribute?” - roles say what a thing is and are essentially static; states change at runtime and must be kept in sync by your JavaScript, which is where most ARIA bugs live; properties describe relationships, like which element labels or controls which.
- “How should you label a control?” - a real
<label>oraria-labelledbypointing at visible text, in preference toaria-label. A visible label helps everyone, including voice-control users who need to say what they see, and anaria-labelthat disagrees with the visible text is actively harmful.aria-describedbyis for the supplementary hint or error message. - “How do you announce something that changed?” - a live region:
aria-live="polite"for most things,assertiveorrole="alert"for errors that must interrupt. The region has to exist in the DOM before you update it - inserting a live region with its content already in place usually announces nothing.