DeepFrontend
Practice ArenaBlogSign in
Go Pro
DeepFrontend

Interview-grade learning paths and hands-on practice for mid-to-senior engineers. Master internals, patterns, and system architecture with runnable code.

All systems operational

Core Courses

  • -JavaScript Internals
  • -React Reconciliation
  • -TypeScript rigor
  • -Next.js Caching
  • -Node.js Event Loop
  • -System Design
  • -Web Security
  • -CSS & Page Layouts

Explore

  • Learning Paths
  • Practice Arena
  • Pricing Options
  • HTML Sitemap

Resources

  • Privacy Policy
  • Terms of Service
  • Email Support

© 2026 DeepFrontend. All rights reserved.

Expert learning environments for web engineering teams.

Home/HTML/Accessibility (ARIA)
intermediate25 min read·Updated Jul 2026

Accessibility (ARIA)

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

Related topics

Semantic HTMLForms & ValidationAccessibility Beyond ARIA (WCAG, keyboard nav, screen readers)ProBrowser DevTools MasteryPro

On this page

25m
Knowledge CheckInterview QuestionsCheatsheet

Concept#

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.

The three categories#

Roles — what an element is:

<div role="tablist">
  <button role="tab" aria-selected="true">Tab 1</button>
  <button role="tab" aria-selected="false">Tab 2</button>
</div>
<div role="tabpanel">...</div>

Properties — characteristics that don't usually change (set once):

<button aria-label="Close dialog">×</button>
<input aria-describedby="pwd-hint" type="password" />
<p id="pwd-hint">Must be at least 8 characters</p>

States — characteristics that change over the widget's lifetime:

<button aria-expanded="false" aria-controls="menu">Options</button>
<div id="menu" hidden>...</div>
button.addEventListener("click", () => {
  const isOpen = button.getAttribute("aria-expanded") === "true";
  button.setAttribute("aria-expanded", String(!isOpen));
  menu.hidden = isOpen;
});

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."

Live regions: announcing dynamic changes#

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.

Hiding content correctly#

<!-- 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>
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 


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.


Common Mistakes#

1. ARIA on top of already-semantic elements#

<!-- 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.

3. aria-hidden="true" on focusable content#

<!-- 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#

<!-- Wrong: visible text says "Learn More", but screen reader says "Read our privacy policy" -->
<a href="/privacy" aria-label="Read our privacy policy">Learn More</a>

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 it
const 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.


Best Practices#

  • 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.

Further Resources#

  • W3C ARIA Authoring Practices Guide (APG) — the canonical reference for every common widget pattern (tabs, combobox, dialog, menu, disclosure) with tested keyboard interaction models.
  • MDN — ARIA
  • WebAIM — Introduction to ARIA
  • The A11Y Project
  • Deque University — ARIA
  • NVDA screen reader (free, Windows)

"password"
/>
<p id="pwd-hint">Must be at least 8 characters, one number</p>
0
);
white-space: nowrap;
border: 0;
}
order
flex-direction: row-reverse
  • VoiceOver User Guide (macOS/iOS, built-in)