ARIA Done Right

4 min read source

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:

  1. Don’t use ARIA if a native element/attribute exists with the semantics you need.
  2. Don’t change native semantics unless you must (<button role="heading"> — no).
  3. All interactive ARIA controls must be keyboard-operable.
  4. Don’t put role="presentation"/aria-hidden="true" on a focusable element (creates a focusable-but-invisible-to-AT trap).
  5. 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 behaviorrole="button" on a <div> still needs tabindex="0" + Enter/Space handlers + focus styles. This is the most common misconception.
  • Stale states — forgetting to flip aria-expanded/aria-selected on 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-labelingaria-label on an element that already has visible text overrides it, sometimes hiding the visible label from AT. Don’t double-name.
  • aria-live added at the same time as content may not announce — render the empty region first.
  • title attribute 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) over aria-label (invisible); describedby is for extra hints/errors.”
  • “Live regions announce dynamic changes — polite normally, alert/assertive for errors — and must exist before being updated.”

Cross-references

Further reading