The Rules of ARIA
ARIA has a small set of rules that exist because each one describes a real way people break pages. The underlying one: a wrong ARIA attribute is worse than no ARIA at all, because the browser will faithfully repeat your mistake.
The intent, the obvious build, and why it breaks
Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.
What are the actual rules for using ARIA, and why is wrong ARIA worse than none?
A person is relying on their screen reader to describe an interface accurately. They will act on what it says — press what it calls a button, trust what it calls expanded, skip what it does not mention.
ARIA attributes improve accessibility, so adding more of them makes a page more accessible. When an audit flags something, add the attribute it mentions.
ARIA does not improve anything by being present. It replaces what the browser would have reported with what you say, and the browser has no way to check whether you are telling the truth.
- ARIA does not improve anything by being present. It replaces what the browser would have reported with what you say, and the browser has no way to check whether you are telling the truth.
role="button"on adivannounces a button that cannot be reached or activated by keyboard — the user is told an affordance exists and then cannot use it (Semantics Before ARIA).aria-expanded="false"that is never updated tells every screen-reader user that an open menu is closed, permanently. The interface now actively lies.aria-hidden="true"on a container with a focusable element inside creates the worst possible state: the element is in the tab order and absent from the accessibility tree, so focus lands on something the screen reader cannot describe at all.- Attribute names fail silently.
aria-labeledbywith oneldoes nothing,role="buton"produces a generic node, andaria-checkedon an element with no matching role is ignored. No console error, no visual difference. aria-labelon a plainspanordivis discarded outright — naming is prohibited on generic roles — so the "fix" that made the audit green changed nothing.
What is actually happening
In the browser, not in the framework.
- Rule 1 — use HTML if you can. If a native element provides the role, the state and the behaviour, use it. ARIA is a fallback for what the platform does not model (What Native Elements Already Do).
- Rule 2 — do not change native semantics unless you really have to.
<h2 role="tab">destroys the heading, which is a primary navigation mechanism for screen-reader users. Wrap or nest instead of overriding. - Rule 3 — every interactive ARIA control must be keyboard-operable. A role implies an interaction model; declaring the role is a commitment to implement all of it (Keyboard Operability).
- Rule 4 — do not use `role="presentation"` or `aria-hidden="true"` on a focusable element. Focusable and unreadable is a strictly worse state than either alone.
- Rule 5 — every interactive element needs an accessible name. A node with a role and no name is announced as its role and nothing else: "button", "tab", "link" (The Accessibility Tree).
- Beyond the five: ARIA attributes are only meaningful on the roles that define them, states must be kept in sync with reality on every code path, and roles carry required structure —
role="tablist"expectsrole="tab"children,role="listbox"expectsrole="option", and a missing level breaks the whole widget.
What this makes the browser do
And which of it is avoidable.
- Each ARIA attribute mutation invalidates the element's accessibility node and fires a platform event. Toggling several attributes where one native state change would do multiplies both.
aria-hiddenon a subtree removes that whole subtree from the tree and rebuilds it when the attribute is removed — cheap once, expensive if toggled per interaction.aria-labelledbycreates a dependency: mutating the referenced element forces the referring element's name to be recomputed, so a live-updating label recomputes names on unrelated nodes (The Accessibility Tree).- Avoidable: attributes recomputed and rewritten on every render even when unchanged. Frameworks will happily set
aria-expanded="false"a hundred times a second if you let them (What a Mutation Costs).
The rules, as engineering constraints
Each rule reads like guidance and is really a description of a specific defect that keeps happening. Read the last column first: that is what the rule is protecting the user from.
Note that only rule one is about avoidance. The others assume you have already decided ARIA is necessary and are describing the obligations that come with it.
| Rule | What it means in code | How it gets violated | What the user experiences |
|---|---|---|---|
| Use HTML if you can | Choose the element whose implicit role is the role you want | A div with role="button" because the design system's button was awkward to style | A control they are told exists and cannot operate |
| Do not override native semantics | Do not put a widget role on a heading, a link, or a list | <h2 role="tab"> in a tab strip built from headings | The heading disappears from the heading list they navigate by |
| Interactive ARIA must be keyboard-operable | Every role you declare implies keys you must implement | role="menu" with mouse handlers and no arrow-key support | A menu that announces itself and cannot be moved through |
No presentation or aria-hidden on focusable elements | Use inert, or remove from the tab order, or neither | aria-hidden on a closed drawer that still contains links | Focus lands on something the screen reader cannot describe |
| All interactive elements need a name | A name from content, label, aria-labelledby or aria-label | Icon-only controls with no text and no label | "button", "button", "button" — no way to choose |
Wrong ARIA is worse than none
Absent semantics leave a user with an unlabelled thing, and they investigate: they explore around it, they read nearby text, they try activating it. Wrong semantics remove that instinct. They are told, confidently, what something is and what state it is in, and they act on it.
This is why the highest-value review question for an ARIA attribute is not "is it valid" but "is it still true on every path" — after an error, after a cancel, after Escape, after the async load that reverted the change.
| Trigger | Symptom | Cause | Response |
|---|---|---|---|
| Menu is opened by keyboard, then closed with Escape | Screen reader still reports "expanded" | aria-expanded updated in the click handler only, not in the Escape path | Derive the attribute from the same state the rendering uses, so no path can miss it. |
| Off-canvas navigation is closed | Tab focuses invisible links that announce nothing | aria-hidden="true" applied while the links remain focusable | Use inert on the closed drawer, or display: none — never aria-hidden alone (Focus Management). |
An icon button is given aria-label="Delete item 4821" | Voice control "click delete" does nothing | The accessible name no longer contains the visible text | Keep the visible words at the start of the name; append detail rather than replacing it. |
A data table is styled with display: grid | Row and column navigation stops working | Changing display removed the table semantics in some engines | Restore explicitly with role="table", role="row", role="cell" — a legitimate repair, verified in the tree. |
A developer writes aria-labeledby | Nothing changes; the control stays unnamed | The attribute name is misspelled and unknown attributes are ignored | Lint for unknown aria-* attributes; this class of bug is fully machine-detectable (Accessibility Testing). |
A submit button uses aria-disabled="true" while a form is invalid | Pressing it still submits | aria-disabled is a description; it blocks nothing | Keep aria-disabled for discoverability and return early in the handler — then move focus to the first error (Errors People Can Actually Perceive). |
aria-hidden, aria-disabled, and the traps between them
Two attributes cause a disproportionate share of real-world damage, because both look like they do something they do not. aria-hidden hides from assistive technology and nothing else; aria-disabled announces disabled and blocks nothing. Both are useful when that is exactly what you want, and both are harmful when used as a substitute for the thing that actually removes the element or the action.
The comparison below is the pattern that generates the worst single state in this module: focusable, invisible, and undescribable.
<button type="submit" aria-disabled="true" class="is-disabled">
Save
</button>
<!-- Focusable and discoverable: good.
Still submits when pressed: not good.
The class dims it, so sighted users never find out. --><button type="submit" aria-disabled="true" class="is-disabled">
Save
</button>
<script>
form.addEventListener('submit', (e) => {
if (!isValid(state)) {
e.preventDefault()
// Do not just refuse. Send the user to the problem.
document.getElementById('error-summary')?.focus()
return
}
// …
})
</script>aria-disabled is the right choice here — a truly disabled button leaves the tab order, so a keyboard user tabbing to the end of a form finds nothing and gets no explanation. But the attribute only describes; the handler must actually refuse, and refusing without moving focus to the reason leaves the user pressing a button that silently does nothing (Errors People Can Actually Perceive).
1<!-- 1. Removed from the tree, removed from tab order, removed from view.2 The safe default when something should not exist right now. -->3<div class="drawer" hidden>…</div>4 5<!-- 2. The worst state in this lesson: absent from the accessibility6 tree, present in the tab order. Focus lands here and the screen7 reader has nothing to say. -->8<div class="drawer" aria-hidden="true" style="transform: translateX(-100%)">9 <a href="/settings">Settings</a>10</div>11 12<!-- 3. inert: not focusable, not clickable, not in the tree — but still13 rendered. This is what a modal wants for the rest of the page. -->14<div class="drawer" inert style="transform: translateX(-100%)">15 <a href="/settings">Settings</a>16</div>17 18<!-- 4. Decoration only. Correct and common: the icon carries no19 information the button's name does not already carry. -->20<button type="button">21 <svg aria-hidden="true" focusable="false"><use href="#trash" /></svg>22 Delete23</button>Case 2 is not a hypothetical — it is what almost every hand-rolled off-canvas menu does, because aria-hidden is the attribute people reach for when they mean "not available yet". inert is the attribute that means that.
How to build it
Most important first.
- Default to no ARIA. A page of correct HTML with no ARIA attributes at all is the target state, not a gap (Semantics Are Behaviour).
- When you add a role, write the full pattern specification with it — keys, focus, states, announcements — and treat missing entries as unfinished work (Accessible Component Patterns).
- Bind ARIA state to the same source of truth as the visual state, in the same expression. If
aria-expandedand the CSS class can disagree, they eventually will. - Prefer relationship attributes on correct elements —
aria-describedby,aria-current,aria-expandedon a real button — over role overrides. These are additive and low-risk. - Use
inertrather thanaria-hiddenwhen the intent is "this part of the page is not available", becauseinertremoves focusability too and cannot produce the focusable-but-hidden state (Focus Management). - Lint for the mechanical failures — invalid roles, unknown attributes, dangling
idreferences,aria-hiddenon focusable elements — and then verify the non-mechanical ones by hand, because no linter can check whether a name is true.
Keyboard, focus, semantics, announcement
A required field on every lesson in this domain, not a section added when there is room.
- Wrong ARIA is not a neutral omission. A missing role degrades to "unlabelled thing" and the user investigates; a wrong role produces confident, false information the user acts on.
- Roles carry required structure and required states.
role="tab"withoutaria-selected,role="option"outside alistbox, orrole="tabpanel"with no associated tab produce widgets that announce as broken. aria-disabledversusdisabledis a real design decision:aria-disabledkeeps the control focusable and discoverable, which is often better for a submit button in a form with errors — but you must then block the action in the handler yourself.- Screen readers implement ARIA with differing completeness. A pattern that reads correctly in one product may be silent in another, which is why any custom widget needs testing against more than one.
What can go wrong
- Stale state:
aria-expanded,aria-selected,aria-checkedandaria-busyupdated on the happy path and left behind on the error path, the cancel path, or the escape-key path. aria-labeloverriding visible text with something different, breaking voice control — the user says the words they can see and nothing happens.role="presentation"on a table or list, which strips the structural relationships the AT uses for navigation while leaving the content in place.aria-liveon a container that also getsaria-hiddenwhen collapsed: the region is not in the tree when the update happens, so nothing is announced (Live Regions and Announcement).- The mitigation failing: an automated rule set is added to CI, everything goes green, and the team concludes the page is accessible. The rules verified attribute syntax, not truth (Accessibility Testing).
aria-disabledused as though it weredisabled: the element stays focusable and clickable, and the handler still fires, so the action still happens.
- State attribute updates that lag the visual change: CSS transitions the panel open immediately while
aria-expandedflips in a later effect, so a screen reader reads a state that is already wrong. - A live region added and populated in the same task, so the AT may never have observed the region existing before its content changed (Live Regions and Announcement).
- Optimistic UI that sets
aria-checkedbefore the server confirms, then reverts — announcing a state change twice, in opposite directions (Optimistic UI).
- ARIA attributes rendered from user-controlled data inject text straight into what the screen reader says. An attacker-supplied
aria-labelcan describe a "Cancel" button as "Confirm" (Cross-Site Scripting). - Nothing expressed in ARIA is enforcement.
aria-disabled,aria-readonlyandaria-hiddenare descriptions of an interface, and the request they were meant to prevent can still be sent (Authorization-Aware UI). aria-hiddenis not a privacy mechanism: the content remains in the DOM, in the page source, and in view of anything reading the document.
- "More ARIA means more accessible." ARIA is a description. More description that is not true is worse, not better.
- "The audit is green so the ARIA is right." Automated rules check syntax and structure. Whether
aria-labelsays the right thing is not machine-decidable. - "
aria-hiddenhides it from everyone." It hides it from assistive technology only, and leaves it visible, focusable and clickable. - "
rolemakes the element behave like that role." It makes the browser *say* so. Behaviour is yours, all of it (Semantics Before ARIA). - "
aria-disableddisables the control." It announces the control as disabled. The click handler still runs.
Measuring it, and what changes in the field
- DevTools accessibility panes show the computed role, name and full ARIA state — the fastest check that the attribute you wrote is the attribute the browser used.
- Automated rule sets catch the mechanical failures well: invalid roles, unsupported attribute/role combinations, dangling references,
aria-hiddenon focusable elements. Run them in CI and expect them to find a minority of real defects. - The truth check is manual: focus the control, listen to what is announced, act on it, and confirm the state changed the way it said it would.
- A grep for
role=andaria-in a diff is a useful review trigger — each occurrence should have a reason and a matching keyboard implementation.
- Under an older screen reader or browser, a newer role may be unsupported and fall back to generic, so progressive-enhancement thinking applies to ARIA too.
- In a design system, one wrong role is replicated everywhere at once — which also means one fix repairs everything, provided components are not forked (Design Systems).
- During hydration, ARIA state rendered on the server can disagree with the client's first render, and the mismatch is announced before it is corrected (Hydration Mismatch).
- In a translated interface,
aria-labelstrings are frequently missed by the localisation pipeline, leaving controls named in the original language (Internationalization).
- Following the rules strictly sometimes means shipping a plainer control than the design asked for. That is the trade being made, and it is usually the right one.
- Rich ARIA widgets have real value where the platform has no equivalent, and they carry a permanent testing obligation across browser and screen-reader combinations — a per-release cost, not a one-off.
aria-disabledkeeps a control discoverable at the cost of you having to suppress the action yourself in every handler, including the ones added later by someone who did not know.
Where this applies
Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.
- GENERALThe rules and the role/attribute vocabulary are specified in ARIA itself and implemented by every engine. Which attributes are valid on which roles is normative, not a per-browser convention.
- PLATFORM-SPECIFICSupport for individual roles and states varies by screen reader more than by browser: NVDA, JAWS and VoiceOver differ in what they announce for
aria-current, forrole="feed", and for less common composite roles, so a widget verified in one may be partly silent in another. Newer roles are frequently the ones with the widest divergence. - SPEC-EVOLVINGARIA is versioned and actively changing: naming prohibitions on generic roles,
aria-description, and the treatment ofaria-ownshave all moved between revisions, and browsers ship the changes at different times. Check the current specification rather than a remembered rule for anything at the edges.
Where the depth lives
This domain teaches the browser-side mechanism and hands the rest off.
- — Testing & Reliability Engineering — the coverage boundary: automated ARIA rule sets are excellent at the machine-decidable subset and cannot evaluate truth, so a test strategy that stops at the linter has measured the wrong thing.