ARIA fills the gaps semantic HTML can't cover — custom widgets, dynamic state, live regions. But the first rule of ARIA is: don't use ARIA if a native element already does the job. This topic covers roles, states, properties, and live regions with the restraint they require.
htmlaccessibilityariaa11yscreen-readers
Knowledge Check
4 questions · pass at 70%
0/4
easymcq
1. What is 'the first rule of ARIA'?
Interview Questions
3 questions
Cheatsheet
Download-ready reference
Cheatsheet
Roles / Properties / States:
role="..." what an element IS (tab, dialog, alert, tablist...)
aria-label="..." accessible NAME from a string (no visible DOM text)
aria-labelledby="id" accessible NAME from another element's text
aria-describedby="id" accessible DESCRIPTION, read after the name
aria-expanded="bool" STATE — must be updated via JS on every toggle
aria-selected="bool" STATE — for tabs/options
aria-checked="bool" STATE — for custom checkboxes/switches
aria-hidden="true" remove from accessibility tree ONLY (still visible/focusable unless tabindex=-1 too)
aria-live="polite|assertive" announce dynamic content changes
aria-atomic="true" announce the whole region, not just the changed part
role="alert" = aria-live="assertive" + aria-atomic="true" (shorthand)
role="status" = aria-live="polite" + aria-atomic="true" (shorthand)
Hiding content:
hidden attribute / display:none / visibility:hidden → hidden from EVERYONE
aria-hidden="true" → hidden from AT only, still visible
.sr-only CSS pattern → visible to AT only, hidden visually
First rule of ARIA:
Native element exists with the right semantics? → use it, no ARIA needed
No native element covers this widget? → ARIA + manual keyboard/focus JS
Follow the APG pattern for that widget type → apg.w3.org
Testing checklist for custom widgets:
[ ] Fully operable by keyboard alone (no mouse)
[ ] Focus visible at every step, never trapped unintentionally
[ ] State attributes (aria-expanded etc.) update on every change, verified in DevTools
[ ] Tested with a real screen reader (VoiceOver / NVDA), not just DevTools a11y tree
[ ] Matches the relevant ARIA APG pattern's keyboard model
ARIA (Accessible Rich Internet Applications) is a set of HTML attributes that describe roles, states, and properties for assistive technology — used when semantic HTML alone can't express what's happening, typically in custom interactive widgets (a tab panel, a combobox, a modal, a live-updating notification).
The first rule of ARIA (literally rule #1 in the W3C ARIA Authoring Practices): don't use ARIA if a native HTML element or attribute already provides the semantics you need. ARIA only changes what's announced — it adds zero actual behavior. Adding role="button" to a <div> makes a screen reader say "button," but you still have to manually add tabindex="0", keyboard handlers for Enter/Space, and focus styling yourself. A real <button> gives you all of that automatically. ARIA is for filling gaps, not a replacement for correct HTML.
aria-expanded must be kept in sync with actual state via JS — ARIA attributes don't update themselves. This is the most common real-world ARIA bug: the attribute is set once in markup and never updated when state actually changes.
Naming: aria-label vs aria-labelledby vs aria-describedby#
<!-- aria-label: accessible name from a string not visible in the DOM --><button aria-label="Close">×</button><!-- aria-labelledby: accessible name from another element's text content --><h2 id="billing-heading">Billing address</h2><section aria-labelledby="billing-heading">...</section><!-- aria-describedby: supplementary description, announced *after* the name --><input aria-describedby="pwd-hint" type=
aria-label and aria-labelledby both set the accessible name (what the element is called). aria-describedby sets the accessible description (additional context, read after the name, not instead of it). A common mistake is using aria-describedby where aria-labelledby was needed — the element ends up with no name at all, just a description, which most screen readers announce as "unlabeled."
Static ARIA describes elements that exist when the page loads. Live regions announce content that changes after the page has loaded — a toast notification, a form error that appears after failed submission, a "3 new messages" counter.
<div aria-live="polite" id="status"></div>
document.getElementById("status").textContent = "Item added to cart";
aria-live="polite" — announced when the screen reader is next idle, doesn't interrupt current speech. Use for most non-urgent updates (search result counts, save confirmations).
aria-live="assertive" — interrupts immediately. Reserve for critical/time-sensitive information (form validation errors, session timeout warnings). Overusing assertive is a common accessibility anti-pattern — it's jarring and can cause a screen reader to cut off whatever the user was doing.
role="alert" — shorthand equivalent to aria-live="assertive" aria-atomic="true", commonly used for form errors.
role="status" — shorthand equivalent to aria-live="polite" aria-atomic="true".
The live region element must already exist in the DOM before its content changes — if you create the element and set its text in the same operation, most screen readers won't detect it. Create the (empty) live region on page load, then mutate its content later.
<!-- Hidden from everyone, including screen readers --><div hidden>...</div><div style="display: none">...</div><!-- Hidden visually, but still announced by screen readers ("visually hidden" pattern) --><span class="sr-only">Opens in a new tab</span><!-- Hidden from screen readers, but visually present (rare — e.g. decorative icons) --><svg aria-hidden="true">...</svg>
display: none/visibility: hidden/the hidden attribute all remove content from both the visual and accessibility tree. aria-hidden="true" removes it only from the accessibility tree (still visible) — correct for decorative icons next to text, wrong if applied to genuinely interactive or informative content, which becomes invisible to screen reader users while remaining visible to everyone else.
<!-- Redundant and can actually break some browser/AT combinations --><button role="button">Submit</button><nav role="navigation">...</nav>
<button> already has an implicit role of button. Adding role="button" is redundant at best.
2. Setting aria-expanded/aria-selected/etc. once and never updating it#
<!-- Static in markup, never changes --><button aria-expanded="false">Menu</button>
If the JS click handler toggles a CSS class but never updates the aria-expanded attribute, a screen reader user is told the menu is permanently closed even while it's open on screen. This is the single most common real-world ARIA bug — visually correct, accessibility-tree wrong.
<!-- Wrong — button is aria-hidden but still tab-focusable --><button aria-hidden="true" onclick="doThing()">Click</button>
This creates a "phantom" focus stop: keyboard users can Tab to it and screen readers announce nothing, or announce it inconsistently across browsers. If content is aria-hidden, it must also not be in the tab order (tabindex="-1") — or better, don't hide interactive content at all.
4. Using aria-label to override visible text with different text#
WCAG 2.5.3 (Label in Name) requires the accessible name to contain the visible text, so voice-control users (who say the visible label to activate it) aren't broken. If you need more context for screen reader users, append it: aria-label="Learn more about our privacy policy".
5. Overusing role="presentation"/aria-hidden to "clean up" the accessibility tree#
Stripping semantics from real content because the accessibility tree "looks messy" in DevTools usually means the underlying markup is wrong, not that ARIA needs to hide it. Fix the HTML structure instead of suppressing the symptom.
6. Forgetting aria-live regions must exist before content changes#
// Wrong — creates and fills in one step, many screen readers miss itconst el = document.createElement("div");el.setAttribute("aria-live", "polite");el.textContent = "Saved!";document.body.appendChild(el);
Create the empty live region ahead of time (on initial render), then mutate .textContent later.
Rule #1: prefer native HTML. Only reach for ARIA to fill a genuine gap — custom widgets, dynamic state, live regions.
Every ARIA state attribute you set must be kept in sync with reality via JS, on every state change, not just initial render.
Test with an actual screen reader before shipping custom widgets — VoiceOver (macOS/iOS, free, built-in) or NVDA (Windows, free) at minimum. DevTools' accessibility tree tells you what should be announced, but only a real screen reader tells you what's actually announced, and rendering differs across AT/browser combinations.
Follow the ARIA Authoring Practices Guide (APG) for any custom widget pattern (combobox, tabs, dialog, menu) rather than inventing your own ARIA usage — these patterns are tested against real screen readers and codify the expected keyboard behavior too.
Use aria-live="polite" by default; reserve assertive for genuinely urgent, time-sensitive announcements.
Don't fight the DOM order for reading order. Screen readers generally follow DOM order regardless of CSS visual order (, , absolute positioning) — a visual reorder via CSS without a matching DOM reorder creates a mismatch between what's seen and what's heard/tab-ordered.
W3C ARIA Authoring Practices Guide (APG) — the canonical reference for every common widget pattern (tabs, combobox, dialog, menu, disclosure) with tested keyboard interaction models.