Overview
ARIA changes what assistive technology announces; it adds no behavior and no keyboard support. WAI-ARIA 1.2 is the current W3C Recommendation (1.3 is a Working Draft as of June 2026), and the ARIA in HTML spec says which roles and attributes are allowed on which elements. Start with a native element and add ARIA only where it falls short.
Follow the four rules of ARIA use
- If a native element or attribute has the semantics and behavior you need, use it.
- Do not change native semantics unless you must;
role="presentation"on a data<table>removes what screen readers need. - Every interactive ARIA control must work with the keyboard.
- Never put
role="presentation"oraria-hidden="true"on a focusable element; users land on “nothing”.
Also avoid aria-label on a bare <div> or <span>: generic elements cannot be named, so the label is ignored by design.
Prefer native widgets
| Need | Use |
|---|---|
| Action | <button>, not <div role="button" tabindex="0"> |
| Checkbox | <input type="checkbox"> |
| Modal | <dialog> with showModal() |
| Disclosure | <details> and <summary>; add name for an exclusive accordion |
| Popup or menu surface | the popover attribute |
| Navigation | <nav> |
<dialog> and <details> are Baseline widely available; the details name attribute is newly available (September 2024), as is popover (January 2025). A modal <dialog> supplies the dialog role, inert background, Esc to close, and focus return.
Learn roles, states, and properties
Roles say what a widget is (dialog, tab), and rarely change. States change on interaction (aria-expanded, aria-checked, aria-selected). Properties describe fixed characteristics (aria-label, aria-haspopup, aria-required). Toggle states from script; set properties once.
Announce async updates with live regions
The live region must exist in the DOM before its content changes. role="status" implies aria-live="polite" and aria-atomic="true"; role="alert" implies assertive. Reserve alerts for errors that need immediate attention.
<div role="status">3 results found.</div>For one-off announcements without a DOM region, element.ariaNotify("Saved") queues speech; pass { priority: "high" } to interrupt. It is Baseline newly available (September 2026), so feature-detect it and keep a live region as the fallback.
Give custom dialogs name, modality, and focus handling
A role="dialog" element needs an accessible name (aria-labelledby), aria-modal="true", focus moved inside on open, focus trapped while open, and focus returned to the trigger on close. aria-modal alone does not trap focus. Prefer <dialog>, which does this natively. A <button command="show-modal" commandfor="dlg"> can open it without script (invoker commands, newly available December 2025).
Wire tabs with roving tabindex
Use role="tablist" on the container, role="tab" on each button, and role="tabpanel" on each panel, linked by aria-controls and aria-labelledby. Only the selected tab is in the Tab order (tabindex="0"); others use tabindex="-1". Arrow keys move between tabs, Home and End jump to the ends, and the panel gets tabindex="0" if it holds no focusable content. Follow the WAI-ARIA Authoring Practices tab pattern exactly.
<div role="tablist" aria-label="Product features">
<button role="tab" id="t1" aria-selected="true" aria-controls="p1">Overview</button>
<button role="tab" id="t2" aria-selected="false" aria-controls="p2" tabindex="-1">Specs</button>
</div>
<div role="tabpanel" id="p1" aria-labelledby="t1">Overview content.</div>
<div role="tabpanel" id="p2" aria-labelledby="t2" hidden>Specs content.</div>Avoid a hand-built listbox or combobox
A custom role="listbox" must handle Up, Down, Home, End, type-ahead, Esc, and aria-activedescendant. A half-built one is worse than a plain <select>. If you cannot commit to the full pattern, style a native <select> or adopt a tested library component. See react for state handling.