AccessibilityGENERALPLATFORM-SPECIFICSPEC-EVOLVING

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.

The question

What are the actual rules for using ARIA, and why is wrong ARIA worse than none?

The user intent

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.

The obvious build

ARIA attributes improve accessibility, so adding more of them makes a page more accessible. When an audit flags something, add the attribute it mentions.

Why it breaks

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.

How it breaks in a real browser
  • 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 a div announces 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-labeledby with one l does nothing, role="buton" produces a generic node, and aria-checked on an element with no matching role is ignored. No console error, no visual difference.
  • aria-label on a plain span or div is discarded outright — naming is prohibited on generic roles — so the "fix" that made the audit green changed nothing.
IntentEventStateUI LogicDOM WorkNetworkLayout / PaintPixelsFeedback

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" expects role="tab" children, role="listbox" expects role="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-hidden on 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-labelledby creates 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.

RuleWhat it means in codeHow it gets violatedWhat the user experiences
Use HTML if you canChoose the element whose implicit role is the role you wantA div with role="button" because the design system's button was awkward to styleA control they are told exists and cannot operate
Do not override native semanticsDo not put a widget role on a heading, a link, or a list<h2 role="tab"> in a tab strip built from headingsThe heading disappears from the heading list they navigate by
Interactive ARIA must be keyboard-operableEvery role you declare implies keys you must implementrole="menu" with mouse handlers and no arrow-key supportA menu that announces itself and cannot be moved through
No presentation or aria-hidden on focusable elementsUse inert, or remove from the tab order, or neitheraria-hidden on a closed drawer that still contains linksFocus lands on something the screen reader cannot describe
All interactive elements need a nameA name from content, label, aria-labelledby or aria-labelIcon-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.

ARIA defects, by what the user is told
TriggerSymptomCauseResponse
Menu is opened by keyboard, then closed with EscapeScreen reader still reports "expanded"aria-expanded updated in the click handler only, not in the Escape pathDerive the attribute from the same state the rendering uses, so no path can miss it.
Off-canvas navigation is closedTab focuses invisible links that announce nothingaria-hidden="true" applied while the links remain focusableUse 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 nothingThe accessible name no longer contains the visible textKeep the visible words at the start of the name; append detail rather than replacing it.
A data table is styled with display: gridRow and column navigation stops workingChanging display removed the table semantics in some enginesRestore explicitly with role="table", role="row", role="cell" — a legitimate repair, verified in the tree.
A developer writes aria-labeledbyNothing changes; the control stays unnamedThe attribute name is misspelled and unknown attributes are ignoredLint 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 invalidPressing it still submitsaria-disabled is a description; it blocks nothingKeep 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.

Disabling a submit button while the form is invalid
aria-disabled with no guard
<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. -->
aria-disabled plus an early return plus somewhere to go
<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).

Four ways to make something unavailable, and what each one actually does
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 accessibility
6 tree, present in the tab order. Focus lands here and the screen
7 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 treebut still
13 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 no
19 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 Delete
23</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-expanded and the CSS class can disagree, they eventually will.
  • Prefer relationship attributes on correct elements — aria-describedby, aria-current, aria-expanded on a real button — over role overrides. These are additive and low-risk.
  • Use inert rather than aria-hidden when the intent is "this part of the page is not available", because inert removes focusability too and cannot produce the focusable-but-hidden state (Focus Management).
  • Lint for the mechanical failures — invalid roles, unknown attributes, dangling id references, aria-hidden on 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" without aria-selected, role="option" outside a listbox, or role="tabpanel" with no associated tab produce widgets that announce as broken.
  • aria-disabled versus disabled is a real design decision: aria-disabled keeps 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

Failure modes
  • Stale state: aria-expanded, aria-selected, aria-checked and aria-busy updated on the happy path and left behind on the error path, the cancel path, or the escape-key path.
  • aria-label overriding 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-live on a container that also gets aria-hidden when 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-disabled used as though it were disabled: the element stays focusable and clickable, and the handler still fires, so the action still happens.
What can arrive out of order
  • State attribute updates that lag the visual change: CSS transitions the panel open immediately while aria-expanded flips 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-checked before the server confirms, then reverts — announcing a state change twice, in opposite directions (Optimistic UI).
Security
  • ARIA attributes rendered from user-controlled data inject text straight into what the screen reader says. An attacker-supplied aria-label can describe a "Cancel" button as "Confirm" (Cross-Site Scripting).
  • Nothing expressed in ARIA is enforcement. aria-disabled, aria-readonly and aria-hidden are descriptions of an interface, and the request they were meant to prevent can still be sent (Authorization-Aware UI).
  • aria-hidden is not a privacy mechanism: the content remains in the DOM, in the page source, and in view of anything reading the document.
Misreads
  • "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-label says the right thing is not machine-decidable.
  • "aria-hidden hides it from everyone." It hides it from assistive technology only, and leaves it visible, focusable and clickable.
  • "role makes the element behave like that role." It makes the browser *say* so. Behaviour is yours, all of it (Semantics Before ARIA).
  • "aria-disabled disables the control." It announces the control as disabled. The click handler still runs.

Measuring it, and what changes in the field

How you would see this
  • 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-hidden on 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= and aria- in a diff is a useful review trigger — each occurrence should have a reason and a matching keyboard implementation.
Slow device, slow network, large data, old tab
  • 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-label strings are frequently missed by the localisation pipeline, leaving controls named in the original language (Internationalization).
What this costs
  • 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-disabled keeps 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, for role="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 of aria-owns have 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.

Domains that do not exist yet
  • 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.