ARIA changes the tree, and nothing else
role, aria-* and their friends edit what assistive technology is told about an element. They add no behaviour whatsoever: no keyboard handling, no focus, no click, no styling. role="button" on a div makes a screen reader say "button" about something that still does nothing when you press Enter.
That is the source of most ARIA bugs: it is a promise about behaviour you then have to keep yourself.
The five rules, as they are actually written
- 1. Do not use ARIA if a native element with the semantics and behaviour you need already exists.
<button>overrole="button", every time. - 2. Do not change native semantics unless you really must.
<h2 role="tab">throws away the heading; wrap instead:<div role="tab"><h2>…</h2></div>. - 3. Every interactive ARIA control must be usable with the keyboard — and with the keys its pattern specifies.
- 4. Do not put
role="presentation"oraria-hidden="true"on a focusable element. You get something that is reachable by Tab and invisible to the person tabbing to it. - 5. Every interactive element needs an accessible name.
A wrong role is worse than none: it tells a confident lie. WebAIM’s annual survey finds home pages with ARIA average more detected errors than pages without — not because ARIA is bad, but because it is usually applied without the behaviour it promises.
What it is genuinely for
| Need | ARIA |
|---|---|
| A state HTML has no attribute for | aria-expanded, aria-selected, aria-current="page" |
| A relationship between elements | aria-labelledby, aria-describedby, aria-controls, aria-owns |
| A widget HTML does not have | role="tablist", role="combobox", role="tree" |
| Announcing a change | role="status", role="alert", aria-live |
| Hiding decoration | aria-hidden="true" — on things that are not focusable |
States that must be kept true
An aria-expanded that never changes is worse than none: it says "collapsed" while the panel is open. Anything you assert in ARIA has to be updated in the same function that changes the reality.
<button aria-expanded="false" aria-controls="panel">Specification</button>
<div id="panel" hidden>…</div>
<script>
button.addEventListener("click", () => {
const open = button.getAttribute("aria-expanded") === "true";
button.setAttribute("aria-expanded", String(!open)); // update BOTH
panel.hidden = open;
});
</script>
The mistakes to recognise
role="button"on an actual<button>— redundant, and a sign somebody is guessing.aria-labelon a plain<div>or<span>with no role: most of the time it is simply ignored.aria-hidden="true"on a wrapper that contains a link or a button — rule 4, and the commonest serious ARIA bug.role="presentation"on a<table>used for layout — fine, except the table is probably data.aria-liveon something that updates constantly, so the user is interrupted permanently.titleused as the only name: not shown on touch, not read by every reader.