Overview

ARIA (Accessible Rich Internet Applications) is a set of attributes that modify what assistive technology announces. It does not change visual appearance or keyboard behavior. A correct ARIA implementation starts with the right semantic element and adds ARIA only where the native element falls short.

The first rule of ARIA: do not use ARIA

If a native HTML element or attribute provides the semantics you need, use it. Native elements carry implicit roles, states, and properties that assistive technology has understood for decades.

  • Use <button> instead of <div role="button" tabindex="0">.
  • Use <input type="checkbox"> instead of <div role="checkbox" aria-checked="false">.
  • Use <nav> instead of <div role="navigation">.
  • Use <details> and <summary> instead of a manually toggled disclosure widget.

The second rule of ARIA: do not change native semantics unless you have no choice. Adding role="presentation" to a <table> that contains tabular data removes the semantics screen readers need.

Roles, states, and properties are three distinct concepts

  • Roles describe what a widget is: role="dialog", role="tab", role="listbox". Set once; do not change dynamically.
  • States describe the current condition of a widget: aria-expanded="true", aria-checked="true", aria-disabled="true". Change on user interaction.
  • Properties describe characteristics that do not change with state: aria-label="Close", aria-haspopup="listbox", aria-required="true".

Mix them correctly. aria-expanded is a state, so toggle it. aria-label is a property, so set it once.

Live regions announce async updates without focus movement

aria-live="polite" announces content changes at the next pause in speech. Use it for status messages, search results updating in place, and non-critical notifications.

aria-live="assertive" interrupts the current announcement. Reserve it for errors or critical alerts only. Overusing assertive is disruptive.

<div role="status" aria-live="polite" aria-atomic="true">
  3 results found.
</div>

aria-atomic="true" causes the entire region to be re-announced as a unit when any part changes, instead of announcing only the changed node. Use it when partial announcements would be confusing.

Dialogs require role, label, and focus management

A modal dialog needs role="dialog", an accessible name, and focus trapped inside it while open.

<div role="dialog" aria-modal="true" aria-labelledby="dialog-title">
  <h2 id="dialog-title">Confirm deletion</h2>
  <p>This action cannot be undone.</p>
  <button>Cancel</button>
  <button>Delete</button>
</div>

When the dialog opens, move focus to the first focusable element or to the dialog element itself. When it closes, return focus to the trigger. Without this, keyboard users lose their place. See accessibility for the keyboard trap requirement.

Expandable sections use aria-expanded on the trigger

Toggle aria-expanded on the button that controls a collapsible region. The aria-controls attribute names the region by id.

<button aria-expanded="false" aria-controls="panel-1">Show details</button>
<div id="panel-1" hidden>
  Details content here.
</div>

Update aria-expanded and toggle hidden (or CSS display: none) together. If the button’s visible label changes between “Show” and “Hide,” the label communicates the current action; aria-expanded still communicates the current state, and both are useful to assistive tech.

Tabs require coordinated roles on three elements

A tab pattern needs role="tablist" on the container, role="tab" on each button, and role="tabpanel" on each panel. Connect each tab to its panel with aria-controls and aria-labelledby.

<div role="tablist" aria-label="Product features">
  <button role="tab" id="tab-1" aria-selected="true" aria-controls="panel-tab-1">Overview</button>
  <button role="tab" id="tab-2" aria-selected="false" aria-controls="panel-tab-2">Specs</button>
</div>
<div role="tabpanel" id="panel-tab-1" aria-labelledby="tab-1">Overview content.</div>
<div role="tabpanel" id="panel-tab-2" aria-labelledby="tab-2" hidden>Specs content.</div>

Keyboard behavior: Tab moves into the tablist; arrow keys move between tabs; Enter or Space activates. Do not reinvent this; follow the WAI-ARIA Authoring Practices tab pattern exactly.

Listbox for custom select-style widgets

When a native <select> is visually insufficient, use role="listbox" with role="option" children. Set aria-selected="true" on the current selection and manage keyboard navigation (Up, Down, Home, End, Enter, Escape) manually.

<ul role="listbox" aria-label="Choose a framework" tabindex="0">
  <li role="option" aria-selected="true" id="opt-1">Astro</li>
  <li role="option" aria-selected="false" id="opt-2">Next.js</li>
</ul>

A half-built listbox that does not handle keyboard input is worse than a plain <select>. If you cannot commit to the full pattern, use a styled native <select>. For the React-specific state management that these patterns need, see react.