Advanced 45 min read Code examples

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-labelPropertyProvide an accessible name when visible label isn't present or sufficient
aria-labelledbyPropertyReference another element's text as the accessible name
aria-describedbyPropertyReference helper text or additional description for an element
aria-expandedStateToggle open/closed state of accordions, dropdowns, disclosures
aria-hiddenStateHide decorative content from assistive technology
aria-selectedStateSelected state in tabs, listboxes, trees
aria-checkedStateChecked state for custom checkboxes, radio buttons, switches
aria-disabledStateCommunicates disabled state while keeping element focusable
aria-requiredPropertyMark required form fields programmatically
aria-invalidStateIndicate a field with invalid input (true/false/grammar/spelling)
aria-currentStateCurrent item in navigation (page/step/date/location/true)
aria-haspopupPropertyIndicate a popup will appear (menu/listbox/tree/grid/dialog)
aria-controlsPropertyReference the element a control affects (use sparingly — poor support)
aria-valuemin/max/nowProperty/StateRange 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 on a div with no role

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.

Setting aria-hidden="true" on the body

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.

Using aria-label to translate content

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.

Overusing aria-describedby

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.

Setting aria-expanded on the wrong element

aria-expanded should be on the button that controls the disclosure, not on the disclosed content itself.

Not updating states dynamically

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