ARIA

ARIA Roles — Practical Guide Demos

5 interactive examples

Landmark roles — page structure map

Every colored region below represents a landmark. Screen reader users can jump directly between them. Click any region to see which HTML element or role created it, and whether the role attribute is needed.

Page header role="banner"
Logo · Site-wide nav · Search · Login
Main content role="main"
Article · Blog posts · App content
Only ONE <main> per page
Breadcrumb nav role="navigation"
Use aria-label to distinguish multiple navs
Sidebar role="complementary"
Related links
Ads · Widgets
--:--:--Click any region to see how the landmark role is created

The one rule for landmark roles

HTML5 semantic element = no role needed. Div or span = role required. Adding role="navigation" to a <nav> is redundant noise in the accessibility tree.

✅ <nav>…</nav> ✅ <main>…</main> ✅ <header>…</header> ✅ <aside>…</aside> ✅ <footer>…</footer>
❌ <nav role="navigation"> ❌ <main role="main"> ❌ <header role="banner"> ❌ <aside role="complementary"> ❌ <footer role="contentinfo">

Widget roles with correct keyboard patterns

Each widget below uses the correct ARIA role + keyboard interaction. Try navigating each with only the keyboard — Tab to reach, then use the widget's specific keys.

role="tab" + role="tablist"

Profile panel content
⌨ Arrow keys change tabs · Tab exits tablist

role="checkbox"

⌨ Space or Enter to toggle

role="button" + aria-pressed

⌨ Enter or Space toggles · aria-pressed="true/false"

Icon buttons with aria-label

❌ No label
✅ aria-label
✅ sr-only text
Click each button to see what SR announces
--:--:--Interact with any widget — see the accessibility announcements

The timing trap — live region must be in DOM BEFORE content injection

Both buttons below try to announce a message. The first creates the live region and injects content simultaneously (broken). The second injects into a pre-existing live region (correct). Watch the SR Announcement box.

SR announcement will appear here…
--:--:--Click either button to see the timing trap in action

Character counter — aria-live="polite" + aria-atomic="true"

Type in the textarea. The counter updates visually and announces to screen readers. aria-atomic="true" ensures the full "X characters remaining" phrase is read, not just the number.

280 characters remaining
<div aria-live="polite" aria-atomic="true" id="live-counter"></div>
--:--:--Type in the textarea — watch the SR announcement

polite vs assertive — when to use each

✓ Use polite for…
Toast confirmations
Character counts
Search result counts
Progress updates
Auto-save status
⚠ Only assertive for…
Payment failures
Session expiry warnings
Critical form errors on submit
Time-sensitive alerts
Click a button to trigger an announcement…

The 5 Rules of ARIA — violated vs correct

The WAI-ARIA Authoring Practices Guide defines 5 rules. Here's what each violation looks like vs the correct pattern. Click any rule to expand.

01 Use native HTML first Most violated
❌ Violation
<div role="button" tabindex="0" onclick="…" onkeydown="…"> Submit </div>
✅ Correct
<button type="button" onclick="…"> Submit </button> // Free: keyboard, ARIA, // focus, :disabled
02 Don't change native semantics unless necessary Common
❌ Violation
<h2 role="tab"> Dashboard </h2> // Strips h2 from // document outline
✅ Correct
<div role="tab"> <h2>Dashboard</h2> </div> // Tab wraps heading. // Both keep their role.
03 All interactive ARIA controls must be keyboard accessible Very common
❌ Violation
<div role="button" onclick="fn()"> Click me </div> // No tabindex. // No keyboard handler. // Mouse-only.
✅ Correct
<div role="button" tabindex="0" onclick="fn()" onkeydown="if(e.key=== 'Enter'||e.key===' ') {e.preventDefault();fn()}"> Click me </div>
04 Don't use role="none" on focusable elements Tricky
❌ Violation
<a href="/home" role="none"> Home </a> // Still focusable. // SR announces nothing. // User is lost.
✅ Correct use
<!-- Only on non-focusable, decorative elements --> <table role="presentation">… <img src="wave.svg" role="none" alt="">
05 All interactive elements must have an accessible name #1 ARIA error
❌ Violation
<button> <svg>…</svg> </button> // SR says: "button" // No name. No context. // User has no idea.
✅ Correct
<button aria-label="Search"> <svg aria-hidden="true"> … </svg> </button> // SR says: "Search, button"
--:--:--Click any rule to expand the violation vs correct code

7 most common ARIA mistakes — with fixes

Backed by WebAIM's 2025 analysis showing that sites with ARIA present have more than twice as many errors as sites without ARIA. Click each to see the mistake and fix.

🔁 Redundant ARIA — adding roles native elements already have Noise

Adding role="button" to a <button>, or role="navigation" to a <nav> creates duplicated noise in the accessibility tree without adding any value.

❌ Redundant
<button role="button">OK</button> <nav role="navigation">…</nav> <a href="#" role="link">Go</a>
✅ Clean
<button>OK</button> <nav>…</nav> <a href="#">Go</a>
🔇 aria-hidden on focusable elements — silent keyboard traps Critical

When focus lands inside an aria-hidden="true" container, the screen reader announces nothing. The user has no idea where they are — a completely silent experience.

❌ Silent trap
<div aria-hidden="true"> <button>Submit</button> </div> // Focus reaches Submit // SR announces nothing
✅ Correct
<div aria-hidden="true"> <!-- decorative only, no focusable children --> <img src="wave.svg" alt=""> </div>
📣 role="menu" on nav dropdowns — 35% cause barriers High impact

role="menu" is for application menus (File, Edit, View). Navigation dropdowns should use no menu role — just aria-expanded on the trigger and plain links in the dropdown.

❌ Wrong role
<ul role="menu"> <li role="menuitem"> <a href="/">Home</a> </li> </ul> // Arrow keys expected // but links behave like links
✅ Correct
<ul id="nav-dd"> <li> <a href="/">Home</a> </li> </ul> // Plain list of links // Tab navigation works
Creating live regions dynamically (timing trap) Very common

The accessibility API must register the live region while empty. Creating a live region and injecting content simultaneously causes no announcement at all.

❌ Never announced
// JS creates + fills at once: const el = document .createElement('div'); el.ariaLive = 'polite'; el.textContent = 'Saved!'; body.appendChild(el);
✅ Pre-registered in HTML
// HTML (on page load): <div id="status" aria-live="polite"> </div> // JS injects later: status.textContent='Saved!';
Forgetting to update aria-expanded on toggle Very common

When a dropdown opens or closes, aria-expanded on the trigger button must be updated. Screen readers announce "expanded" or "collapsed" — without this, users can't tell the state changed.

❌ State not updated
btn.onclick = () => { menu.classList .toggle('open'); // aria-expanded stays false // SR doesn't know it opened };
✅ State synced
btn.onclick = () => { const open = btn.getAttribute( 'aria-expanded' ) === 'true'; btn.setAttribute( 'aria-expanded', !open); menu.hidden = open; };
🏷 Using aria-label on non-interactive, non-landmark elements Confusing

aria-label on a plain <div> or <p> has no effect — these elements have no role that would surface the label. Labels are only meaningful on interactive elements and landmark/grouping roles.

❌ Ignored
<p aria-label="intro"> Welcome… </p> // aria-label has no effect // on <p> — it has no role
✅ Effective
<!-- On interactive or landmark elements only --> <button aria-label="Close"> <nav aria-label="Primary"> <section aria-label="Reviews">
📊 Not checking the Accessibility Tree before testing with SR Efficiency

Open DevTools → Accessibility pane and check three things on every interactive element: (1) Is it in the tree? (2) Does it have the right role? (3) Does it have an accessible name? Fix all three before opening a screen reader.

// In Chrome DevTools: Elements panel → Accessibility tab // What to check: Role: button Name: "Search" State: focusable Name: (empty) ✗ Add aria-label!
--:--:--Click any mistake to expand the violation and fix
Read the tutorial