ARIA Done Right
TL;DR
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.
Interview Q&A
Q: What are the five rules of ARIA (the gist)?
A:
- 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.
Q: Roles vs states vs properties?
A:
- 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.
Q: aria-label vs aria-labelledby vs aria-describedby vs <label>?
A:
| 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).
Q: What are live regions?
A: 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.
Q: What does aria-hidden="true" do, and the trap?
A: 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).
Q: Give an example of ARIA fixing a real gap.
A: 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 / edge cases
- 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.
What a senior is expected to say
- “First rule of ARIA: don’t use ARIA. It changes announcement, not behavior —
role='button'still needs tabindex, key handlers, and focus styles.” - “Roles are what it is, states change at runtime and must stay synced with JS, properties describe relationships.”
- “Prefer
<label>/aria-labelledby(visible) overaria-label(invisible);describedbyis for extra hints/errors.” - “Live regions announce dynamic changes —
politenormally,alert/assertivefor errors — and must exist before being updated.”
Cross-references
- Use native elements first: 02_semantic_html_and_landmarks.md
- Keyboard handlers ARIA widgets require: 04_keyboard_and_focus.md
- Built widgets following APG: 06_accessible_components.md
- Form error announcement with live regions: 05_forms_accessibility.md
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