ARIA Deep Dive
WAI-ARIA is a powerful toolkit for making complex interactive components accessible — but misuse is widespread. Learn when, why, and how to use ARIA correctly.
What is ARIA?
WAI-ARIA (Web Accessibility Initiative — Accessible Rich Internet Applications) is a W3C specification that defines a set of HTML attributes — roles, properties, and states — that can be added to markup to communicate semantic information to assistive technologies that HTML alone cannot express.
ARIA does not change how elements look or behave in the browser — it only affects how they are communicated to the accessibility tree, which assistive technologies read.
ARIA is defined in WCAG 2.2 Principle 4 (Robust), particularly SC 4.1.2 which requires that name, role, and value are programmatically determinable for all UI components.
The First Rule of ARIA
"If you can use a native HTML element or attribute with the semantics and behavior you require already built in, then do so instead of re-purposing an element and adding an ARIA role, state, or property to make it accessible."
— W3C, Using ARIA
In practice: use <button> instead of <div role="button">. Use <nav> instead of <div role="navigation">. ARIA should fill gaps where native HTML falls short — not replace it.
Roles
ARIA roles define what an element is. They are divided into categories:
Landmark Roles
Landmark roles identify major page regions. Most have native HTML equivalents — prefer those.
| ARIA Role | Native Element | Purpose |
|---|---|---|
banner | <header> (page level) | Site header with logo and navigation |
navigation | <nav> | Navigation link groups |
main | <main> | Primary page content |
complementary | <aside> | Supporting content, tangentially related |
contentinfo | <footer> (page level) | Footer with copyright, privacy links |
search | <search> | Search functionality |
Widget Roles (No HTML Equivalent)
These roles communicate complex interactive patterns that have no semantic HTML equivalent:
tab / tablist / tabpanel
Tab widget pattern. The tablist contains tabs; each tab controls a tabpanel.
combobox
An editable input combined with a popup listbox, treeview, or other widget.
tree / treeitem
Hierarchical list widget where items can be expanded/collapsed.
grid / row / gridcell
An interactive widget with cells that can be edited and navigated like a spreadsheet.
slider
A widget that allows selection of a value from a range.
switch
A checkbox-like widget representing on/off states (not binary yes/no).
Document Structure Roles
Used to describe structural relationships when HTML semantics alone are insufficient.
application
article
columnheader
definition
figure
group
heading (aria-level=)
img
list / listitem
math
note
presentation / none
rowheader
term
Properties & States
Properties define characteristics that are unlikely to change (e.g., aria-label). States are dynamic values that change in response to user interaction (e.g., aria-expanded). States must be updated via JavaScript when the UI changes.
| Attribute | Type | Common Use |
|---|---|---|
aria-label | Property | Provide an accessible name when visible label isn't present or sufficient |
aria-labelledby | Property | Reference another element's text as the accessible name |
aria-describedby | Property | Reference helper text or additional description for an element |
aria-expanded | State | Toggle open/closed state of accordions, dropdowns, disclosures |
aria-hidden | State | Hide decorative content from assistive technology |
aria-selected | State | Selected state in tabs, listboxes, trees |
aria-checked | State | Checked state for custom checkboxes, radio buttons, switches |
aria-disabled | State | Communicates disabled state while keeping element focusable |
aria-required | Property | Mark required form fields programmatically |
aria-invalid | State | Indicate a field with invalid input (true/false/grammar/spelling) |
aria-current | State | Current item in navigation (page/step/date/location/true) |
aria-haspopup | Property | Indicate a popup will appear (menu/listbox/tree/grid/dialog) |
aria-controls | Property | Reference the element a control affects (use sparingly — poor support) |
aria-valuemin/max/now | Property/State | Range values for sliders and progress bars |
Live Regions
Live regions announce dynamic content changes to screen reader users without requiring a page refresh or focus move. They are essential for single-page apps, form validation, and status messages (WCAG 2.2 SC 4.1.3).
aria-live="polite"
Announces changes when the user is idle. Use for non-critical updates like "Item added to cart" or search result counts.
aria-live="assertive"
Announces changes immediately, interrupting what the screen reader is saying. Reserve for critical errors only — overuse is highly disruptive.
role="status"
Implied aria-live="polite". For non-critical status updates.
role="alert"
Implied aria-live="assertive". For critical error messages that need immediate attention.
role="log"
For ordered logs like chat, messaging, or activity feeds. New additions are announced politely.
Example: Announcing form submission
<div role="status" aria-live="polite" aria-atomic="true">
<!-- Initially empty -->
</div>
// JavaScript: After form submission
document.querySelector('[role="status"]').textContent =
'Your message has been sent. We\'ll respond within 24 hours.';
Common Patterns
Modal Dialogs
Modals must manage focus — moving it into the dialog on open and returning it to the trigger on close. Focus must be trapped within the dialog while it is open.
<div
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
aria-describedby="dialog-desc"
>
<h2 id="dialog-title">Confirm Deletion</h2>
<p id="dialog-desc">This action cannot be undone.</p>
<button type="button">Cancel</button>
<button type="button">Delete</button>
</div>
Tabs
Tab navigation uses arrow keys to move between tabs (not Tab key). The Tab key moves focus to the selected tabpanel.
<div role="tablist" aria-label="Pricing options">
<button role="tab" aria-selected="true" aria-controls="monthly-panel">Monthly</button>
<button role="tab" aria-selected="false" aria-controls="annual-panel" tabindex="-1">Annual</button>
</div>
<div role="tabpanel" id="monthly-panel" aria-labelledby="monthly-tab">
<!-- Monthly pricing content -->
</div>
Accordions
Disclosure patterns (accordions) use a button to toggle a panel. The button's aria-expanded state must be updated dynamically.
<h3>
<button
aria-expanded="false"
aria-controls="section1-panel"
>
What is web accessibility?
</button>
</h3>
<div id="section1-panel" hidden>
<p>Web accessibility means...</p>
</div>
Custom Dropdowns (Combobox)
Custom select/autocomplete widgets require significant ARIA work. Consider using the native <select> element when possible — it is accessible and well-supported.
<div role="combobox" aria-expanded="false" aria-haspopup="listbox">
<input
type="text"
autocomplete="off"
aria-autocomplete="list"
aria-controls="options-list"
aria-activedescendant=""
/>
<ul role="listbox" id="options-list">
<li role="option" aria-selected="false">Option 1</li>
</ul>
</div>
ARIA Anti-Patterns
These are some of the most common ARIA mistakes that make pages less accessible, not more:
aria-label only works when an element has a role that supports naming. Adding aria-label to a plain div or span has no effect.
This hides the entire page from assistive technology. Often done accidentally when a modal is open. Only the background content should be hidden, not the modal itself.
If an icon button says "en" but needs to mean "English", don't use aria-label="English" while the button visually shows "en". Use a visually hidden span instead — it works for translation and other AT.
aria-describedby adds supplemental description, not a name. Don't use it as a replacement for aria-labelledby or labels. Screen readers may announce it at different times or not at all.
aria-expanded should be on the button that controls the disclosure, not on the disclosed content itself.
ARIA states like aria-expanded, aria-checked, and aria-selected must be updated via JavaScript when the UI changes. Setting them statically in HTML and never changing them is useless.
Testing ARIA
ARIA implementation must be tested with real assistive technology — automated tools cannot verify whether ARIA communicates the right information in practice.
Inspect the Accessibility Tree
Chrome and Firefox DevTools have an Accessibility panel showing the computed accessibility tree. Verify that roles, names, and states are being computed correctly for your ARIA markup.
Test with Screen Readers
Test with NVDA + Chrome, JAWS + Chrome, and VoiceOver + Safari. ARIA support varies significantly between screen reader/browser combinations. What works in one pair may fail in another.
Keyboard Test
ARIA roles often carry implicit keyboard interaction requirements from the ARIA Authoring Practices Guide. Verify that keyboard navigation follows the expected patterns for each widget type.
Automated Checks
axe-core catches some ARIA errors like invalid role values, missing required children, and prohibited attributes. Run it as part of your CI pipeline, but treat it as a supplement to manual testing, not a replacement.
Need an ARIA Code Review?
Our engineers specialize in reviewing custom interactive components and ensuring ARIA is implemented correctly across all major screen reader and browser combinations.
Get a Technical Review