What WAI-ARIA Is
WAI-ARIA (Web Accessibility Initiative — Accessible Rich Internet Applications) is a W3C specification that adds semantic meaning to HTML elements so assistive technologies like screen readers can understand dynamic web content. It fills the gap between what native HTML can express and what modern web applications actually do.
The first rule of ARIA: Don't use ARIA if native HTML does the job. A <button> is always better than <div role="button">. ARIA should enhance, not replace, semantic HTML.
| Version | Released | Status |
|---|---|---|
| WAI-ARIA 1.0 | March 2014 | Superseded |
| WAI-ARIA 1.1 | December 2017 | Recommendation |
| WAI-ARIA 1.2 | June 2023 | Current Recommendation |
| WAI-ARIA 1.3 | In development | Working Draft |
When You Need ARIA
Use ARIA when HTML alone cannot communicate the component's purpose or state to assistive technology:
| Scenario | Native HTML Sufficient? | ARIA Needed? |
|---|---|---|
| Simple button | Yes — <button> |
No |
| Navigation | Yes — <nav> |
No |
| Modal dialog | Partially — no native <dialog> state management |
Yes — aria-modal, focus trap |
| Tab panel | No native element | Yes — role="tablist", role="tab", role="tabpanel" |
| Accordion | No native expand/collapse semantics | Yes — aria-expanded, aria-controls |
| Live notification | No native "content updated" signal | Yes — aria-live |
| Combobox / autocomplete | No native pattern | Yes — role="combobox", aria-activedescendant |
| Progress indicator | Partially — <progress> element exists |
Sometimes — aria-valuenow for custom designs |
Core ARIA Concepts
Roles
Roles tell assistive technology what an element is. There are four categories:
Landmark roles define page regions. Screen reader users jump between landmarks to navigate:
<header role="banner">...</header> <!-- Site header (use <header> instead) -->
<nav role="navigation">...</nav> <!-- Navigation (use <nav> instead) -->
<main role="main">...</main> <!-- Main content (use <main> instead) -->
<aside role="complementary">...</aside> <!-- Sidebar (use <aside> instead) -->
<footer role="contentinfo">...</footer> <!-- Site footer (use <footer> instead) -->
<form role="search">...</form> <!-- Search form (use <search> in HTML5.2+) -->
<!-- ARIA landmark roles are redundant when using semantic HTML -->
<!-- Only add role if you can't use the semantic element -->
Widget roles describe interactive components:
<div role="tablist">
<button role="tab" aria-selected="true" aria-controls="panel-1">Tab 1</button>
<button role="tab" aria-selected="false" aria-controls="panel-2">Tab 2</button>
</div>
<div role="tabpanel" id="panel-1">Content for tab 1</div>
<div role="tabpanel" id="panel-2" hidden>Content for tab 2</div>
Common widget roles: tab, tablist, tabpanel, dialog, alertdialog, menu, menuitem, tree, treeitem, listbox, option, slider, spinbutton, combobox, tooltip.
Document structure roles: article, heading, list, listitem, table, row, cell, img, figure, separator.
Live region roles: alert, log, marquee, status, timer.
States and Properties
States and properties describe the current condition of an element. States change dynamically; properties are relatively static.
Common states (change with interaction):
| Attribute | Purpose | Example |
|---|---|---|
aria-expanded |
Accordion/dropdown open state | aria-expanded="false" → "true" on click |
aria-selected |
Tab or listbox selection | aria-selected="true" on active tab |
aria-checked |
Checkbox/radio state | aria-checked="mixed" for indeterminate |
aria-disabled |
Non-interactive state | aria-disabled="true" (different from HTML disabled) |
aria-hidden |
Hide from assistive technology | aria-hidden="true" on decorative elements |
aria-invalid |
Form validation failure | aria-invalid="true" on error |
aria-pressed |
Toggle button state | aria-pressed="true" when active |
aria-busy |
Content loading/updating | aria-busy="true" during fetch |
Common properties (relatively static):
| Attribute | Purpose | Example |
|---|---|---|
aria-label |
Accessible name (no visible label) | <button aria-label="Close">×</button> |
aria-labelledby |
Points to visible label element | aria-labelledby="heading-id" |
aria-describedby |
Points to descriptive text | aria-describedby="help-text-id" |
aria-controls |
Element this controls | Tab → aria-controls="panel-id" |
aria-owns |
DOM ownership (for reordered content) | Parent → aria-owns="child-id" |
aria-live |
Announce dynamic updates | aria-live="polite" or "assertive" |
aria-required |
Required form field | aria-required="true" |
aria-haspopup |
Indicates popup type | aria-haspopup="menu" |
aria-current |
Current item in a set | aria-current="page" for active nav link |
Common Widget Patterns
These patterns follow the WAI-ARIA Authoring Practices Guide (APG).
Modal Dialog
<div role="dialog" aria-modal="true" aria-labelledby="dialog-title">
<h2 id="dialog-title">Confirm Deletion</h2>
<p>Are you sure you want to delete this item?</p>
<button>Cancel</button>
<button>Delete</button>
</div>
Keyboard requirements:
- Focus moves into the dialog when it opens (to the first focusable element or the dialog itself)
- Tab/Shift+Tab cycles focus within the dialog (focus trap)
- Escape closes the dialog
- Focus returns to the trigger element when the dialog closes
// Focus trap for modal dialog
function trapFocus(dialog) {
const focusable = dialog.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
dialog.addEventListener('keydown', (e) => {
if (e.key === 'Escape') { closeDialog(); return; }
if (e.key !== 'Tab') return;
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
});
first.focus();
}
Accordion
<div class="accordion">
<h3>
<button aria-expanded="false" aria-controls="section1">
Section 1
</button>
</h3>
<div id="section1" role="region" aria-labelledby="section1-btn" hidden>
<p>Section 1 content...</p>
</div>
</div>
Keyboard: Enter/Space toggles the section. Optionally, arrow keys move between accordion headers.
Live Regions
Live regions announce dynamic content updates to screen readers without moving focus.
<!-- Polite: announces after screen reader finishes current speech -->
<div aria-live="polite" aria-atomic="true">
3 items in your cart
</div>
<!-- Assertive: interrupts current speech immediately (use sparingly) -->
<div role="alert">
Your session will expire in 2 minutes.
</div>
<!-- Status: for non-urgent status updates -->
<div role="status">
Search found 42 results.
</div>
aria-live Value |
Behavior | Use For |
|---|---|---|
off (default) |
No announcements | Static content |
polite |
Waits for pause in speech | Cart updates, search results count, form saved |
assertive |
Interrupts immediately | Errors, session expiry, critical alerts |
Important: The live region container must exist in the DOM before you update its content. If you insert the container and content at the same time, the screen reader may not announce it.
Navigation with aria-current
<nav aria-label="Main navigation">
<a href="/" aria-current="page">Home</a>
<a href="/products">Products</a>
<a href="/about">About</a>
</nav>
aria-current="page" tells screen readers which link represents the current page. Other values: step (multi-step process), date (calendar), time, location, true (generic).
Common ARIA Mistakes
1. Redundant ARIA on Semantic HTML
<!-- Bad: redundant -->
<button role="button">Save</button>
<nav role="navigation">...</nav>
<main role="main">...</main>
<!-- Good: native HTML is sufficient -->
<button>Save</button>
<nav>...</nav>
<main>...</main>
2. Using aria-hidden="true" on Focusable Elements
<!-- Bad: screen reader can't see it, but keyboard can still focus it -->
<button aria-hidden="true">Hidden but focusable</button>
<!-- Good: actually remove from tab order too -->
<button aria-hidden="true" tabindex="-1">Properly hidden</button>
<!-- Better: just use CSS or hidden attribute -->
<button hidden>Properly hidden</button>
3. Missing Required ARIA Properties
<!-- Bad: role="checkbox" without aria-checked -->
<div role="checkbox">Accept terms</div>
<!-- Good: required state included -->
<div role="checkbox" aria-checked="false" tabindex="0">Accept terms</div>
<!-- Best: use native HTML -->
<label><input type="checkbox"> Accept terms</label>
4. aria-label Overriding Visible Text
<!-- Bad: screen reader says "Close dialog" but visible text says "X" -->
<!-- This is actually CORRECT for icon-only buttons -->
<button aria-label="Close dialog">×</button>
<!-- Bad: aria-label contradicts visible text -->
<button aria-label="Submit form">Cancel</button>
5. Using ARIA to Fix Layout Problems
<!-- Bad: using ARIA to work around bad HTML structure -->
<div role="heading" aria-level="2">This should be an h2</div>
<!-- Good: use the actual heading element -->
<h2>This should be an h2</h2>
Testing ARIA Implementation
Screen Reader Testing
| Screen Reader | OS | Browser | Free? |
|---|---|---|---|
| VoiceOver | macOS / iOS | Safari | Yes (built-in) |
| NVDA | Windows | Firefox, Chrome | Yes |
| JAWS | Windows | Chrome, Edge | No ($1,000+/year) |
| TalkBack | Android | Chrome | Yes (built-in) |
| Narrator | Windows | Edge | Yes (built-in) |
Quick VoiceOver test (macOS): Cmd+F5 to enable. Use VO keys (Ctrl+Option) + arrow keys to navigate. Listen for role announcements ("button", "tab, selected", "expanded"), state changes, and live region updates.
Automated ARIA Validation
# axe-core catches incorrect ARIA usage
npx @axe-core/cli https://your-site.com
# Common axe ARIA rules:
# aria-allowed-attr — attribute is valid for the role
# aria-required-attr — required attributes are present
# aria-valid-attr-value — attribute values are valid
# aria-roles — role value is valid
# aria-hidden-focus — hidden elements aren't focusable
Browser DevTools
Chrome DevTools → Elements → Accessibility panel shows the computed accessible name, role, and state for any element. Firefox has a similar Accessibility Inspector (F12 → Accessibility tab) that can scan the entire page for issues.
What OpsBlu Tests
OpsBlu's accessibility audit flags ARIA-related issues including:
- Invalid ARIA roles and attributes
- Missing required ARIA properties for widget roles
aria-hidden="true"on focusable elements- Elements with ARIA roles but no accessible name
- Mismatched
aria-labelledbyandaria-describedbyreferences (pointing to non-existent IDs) - Deprecated ARIA attributes
Next Steps
- WCAG Accessibility — the guidelines ARIA helps you meet
- WAI-ARIA Authoring Practices Guide — official patterns with code examples
- WAI-ARIA 1.2 Specification — full attribute reference
- Section 508 Compliance — US federal accessibility requirements
- Deque University — free ARIA training courses