ARIA (Accessible Rich Internet Applications) is a W3C specification of attributes that change how elements are exposed to assistive technology. It lets you describe custom widgets, such as tabs or comboboxes, that HTML has no element for. It is powerful, and it is easy to misuse.
What ARIA does and does not do
ARIA changes only the accessibility tree: what assistive technology is told about an element. It does not change behaviour, appearance or keyboard support.
<div role="button">Save</div>
A screen reader now announces "Save, button". But the div still:
- is not focusable with
Tab; - does nothing when users press
EnterorSpace; - is not submitted with a form or disabled by a
disabledattribute.
You have made a promise to the user that the element cannot keep. That is often worse than no ARIA at all, because users expect a button to work like one.
Adding a role is a promise that you have also built the matching keyboard behaviour. If you are not going to write that JavaScript, do not add the role.
The first rule of ARIA
The W3C document Using ARIA sets out five rules. The first matters most:
If you can use a native HTML element or attribute with the semantics and behavior you require already built in, instead of re-purposing an element and adding an ARIA role, state or property to make it accessible, then do so.
| Need | Use this | Not this |
|---|---|---|
| A button | <button> | <div role="button" tabindex="0"> |
| A link | <a href="..."> | <span role="link"> |
| A checkbox | <input type="checkbox"> | <div role="checkbox" aria-checked="false"> |
| A heading | <h2> | <div role="heading" aria-level="2"> |
| Show and hide | <details> and <summary> | A custom disclosure, when details fits |
| A modal | <dialog> with showModal() | <div role="dialog"> with a hand-built focus trap |
The other rules, in short: do not change native semantics unless you really have to; all interactive ARIA controls must be keyboard operable; do not use role="presentation" or aria-hidden="true" on focusable elements; and all interactive elements must have an accessible name.
WebAIM's annual analysis of the top one million home pages has repeatedly found that pages using ARIA tend to have more detected errors than pages without it. ARIA is not the cause in itself; misused ARIA is.
Three kinds of attribute
Roles
A role says what an element is. Native elements have implicit roles: button is a button, nav is navigation, ul is a list. The role attribute overrides or supplies one.
Roles fall into groups:
- Widget roles:
button,checkbox,tab,tabpanel,combobox,slider,switch. - Composite roles:
tablist,listbox,grid,menu,radiogroup. - Document structure roles:
heading,list,img,table,tooltip. - Landmark roles:
banner,navigation,main,region,search. - Live region roles:
status,alert,log.
States
States describe the current condition and usually change as users interact. You must update them with JavaScript.
aria-expanded="true"or"false": a control that shows and hides content is open or closed.aria-pressed="true"or"false": a toggle button is on or off.aria-selected: which tab or option is selected.aria-checked: for custom checkboxes and switches.aria-current="page": the current item in a set, such as the link to this page in navigation.aria-invalid="true": the value fails validation.aria-disabled="true": the control is disabled but stays focusable.
Properties
Properties describe characteristics and relationships that change less often.
aria-label,aria-labelledbyandaria-describedby: names and descriptions (lesson 8).aria-controls: the element this control affects.aria-haspopup: the control opens a menu, listbox, dialog or similar.aria-live: the element is a live region (lesson 18).aria-hidden="true": remove the element and its children from the accessibility tree.
Example: a toggle button with aria-pressed
HTML has no toggle button, so ARIA fills a real gap. Note that it starts from a native button, which provides focus, keyboard support and the button role. ARIA adds only the state.
<button type="button" id="mute" aria-pressed="false">Mute notifications</button>
<script>
const mute = document.getElementById('mute');
mute.addEventListener('click', () => {
const pressed = mute.getAttribute('aria-pressed') === 'true';
mute.setAttribute('aria-pressed', String(!pressed));
});
</script>
#mute[aria-pressed="true"] {
background: #0b0c0c;
color: #ffffff;
}
A screen reader announces "Mute notifications, toggle button, not pressed", then "pressed" after activation. Keep the label constant. If you change the label to "Unmute" as well, the state becomes contradictory.
Styling from the ARIA attribute, as here, keeps the visual state and the exposed state in step: if you forget to update the attribute, the style breaks too, and you notice.
Example: aria-expanded on a disclosure button
<button type="button" aria-expanded="false" aria-controls="delivery-info">
Delivery information
</button>
<div id="delivery-info" hidden>
<p>Orders placed before 2pm are sent the same day.</p>
</div>
<script>
const toggle = document.querySelector('[aria-controls="delivery-info"]');
const panel = document.getElementById('delivery-info');
toggle.addEventListener('click', () => {
const expanded = toggle.getAttribute('aria-expanded') === 'true';
toggle.setAttribute('aria-expanded', String(!expanded));
panel.hidden = expanded;
});
</script>
The hidden attribute hides the panel from everyone, including screen readers. aria-controls is supported unevenly, so treat it as a helpful extra, not the main mechanism. Lesson 16 builds full accordions and tabs.
aria-hidden: use with care
aria-hidden="true" hides content from assistive technology while leaving it visible. It is right for decorative icons and duplicated content. It is wrong on anything focusable, because keyboard users can then focus an element that screen readers say does not exist.
<!-- Good: hides a decorative icon next to visible text -->
<button type="button"><svg aria-hidden="true" focusable="false" width="16" height="16"></svg> Print</button>
<!-- Bad: the link can still receive focus -->
<a href="/offers" aria-hidden="true">Offers</a>
To hide something from everyone, use hidden or display: none. To block a whole region while an overlay is open, use inert.
Common mistakes
- Adding
role="button"to a link orrole="link"to a button instead of choosing the right element. - Using
aria-labelon adivorspanwith no role. Names are not supported on generic elements, so screen readers often ignore it. - Copying ARIA from a widget pattern without the keyboard handling it requires.
- Using
role="menu"for website navigation (lesson 17 explains why). - Forgetting to update states such as
aria-expandedwhen the UI changes. - Redundant roles such as
<nav role="navigation">or<button role="button">. They are harmless but add noise.
Summary
- ARIA changes only what assistive technology is told; it adds no behaviour.
- First rule of ARIA: use native HTML when it does the job.
- Roles say what something is; states and properties describe its condition and relationships.
- Keep states such as
aria-expandedandaria-pressedin sync with the UI, and style from them. - Never put
aria-hidden="true"on focusable content; usehiddenorinertinstead. - Every ARIA widget must be keyboard operable and named.