ARIA (Accessible Rich Internet Applications) is a set of attributes that lets you describe to assistive technologies what HTML alone cannot express: that an element is a tab in a tabbed panel, that a menu is open, that part of the page has been updated. It is a powerful tool, but used badly it does more harm than good. Its main rule, in fact, is an invitation not to use it.
The first rule of ARIA
The specification on using ARIA in HTML says it clearly: if you can use a native HTML element that already has the semantics and behaviour you need, use it instead of adding ARIA to a generic element.
<!-- Avoid -->
<div role="button" tabindex="0" aria-pressed="false">Bold</div>
<!-- Better -->
<button type="button" aria-pressed="false">Bold</button>
The reason is that ARIA only changes what is announced, not the behaviour. A div with role="button" is announced as a button, but it does not receive focus without tabindex, does not activate with Enter or Space without extra code, and does not take part in form submission. You are promising the user a button that does not behave like one.
The three families of attributes
Roles
They describe what an element is: role="dialog", role="tablist", role="tab", role="alert", role="switch". Many roles already correspond to HTML elements (button, navigation, main, list) and should not be repeated: <nav role="navigation"> is redundant.
Properties
They describe relatively stable characteristics: aria-label, aria-labelledby, aria-describedby, aria-controls, aria-required.
States
They describe conditions that change with interaction: aria-expanded, aria-pressed, aria-checked, aria-selected, aria-invalid, aria-disabled, aria-current. They must be updated via JavaScript every time the real state changes.
Giving elements a name
Every interactive control needs an accessible name: it is what the screen reader speaks. Normally it comes from the visible text. When there is no visible text, ARIA helps:
<button type="button" aria-label="Close dialog">
<svg aria-hidden="true" focusable="false">...</svg>
</button>
aria-labelassigns a name through a string.aria-labelledbyuses the text of another element, referenced by itsid; it takes precedence overaria-label.aria-describedbyadds a supplementary description, read after the name.
A practical tip: if there is visible text, the accessible name should contain it. Voice control users say "click Send" by reading what they see; if the accessible name is different, the command does not work.
A real case: the button that opens a menu
A drop-down menu is the classic place where ARIA genuinely helps, because HTML has no native way to say "this button controls that panel and it is open":
<button type="button" aria-expanded="false" aria-controls="main-menu">
Menu
</button>
<ul id="main-menu" hidden>
<li><a href="/services">Services</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
const btn = document.querySelector('[aria-controls="main-menu"]');
const menu = document.getElementById('main-menu');
btn.addEventListener('click', () => {
const open = btn.getAttribute('aria-expanded') === 'true';
btn.setAttribute('aria-expanded', String(!open));
menu.hidden = open;
});
The aria-expanded state lets the screen reader announce "Menu, button, collapsed" and "expanded". The hidden attribute hides the panel from everyone, not just visually.
For simple expandable panels, before writing any code, consider details and summary: they offer the same behaviour natively.
Live regions: announcing updates
When part of the page changes without the user having moved focus there (a "Saved" message, the number of search results, an error after submission), screen reader users do not notice. Live regions solve this:
<div aria-live="polite" id="status"></div>
document.getElementById('status').textContent = 'Changes saved';
aria-live="polite"announces the change once the user has finished what they are listening to.aria-live="assertive"(orrole="alert") interrupts immediately: reserve it for urgent messages.
The region must be present in the page before the content changes; inserting an already-filled element into the DOM often produces no announcement at all.
Hiding with care
aria-hidden="true" removes an element from the accessibility tree but leaves it visible. It is useful for decorative icons next to text. It must never be placed on elements that can receive focus, nor on a container that includes interactive controls: keyboard users would land on "ghost" elements that the screen reader does not announce.
The most common mistakes
- Adding redundant roles to native elements.
- Using
role="button"orrole="link"ondivandspaninstead of real elements. - Forgetting to update states: a fixed
aria-expanded="false"on an open menu is worse than no attribute at all. - Using
aria-labelon non-interactive elements such as genericdivandspan, where it is often ignored. - Building complex widgets (tabs, comboboxes, grids) without following the keyboard interaction patterns documented in the W3C ARIA Authoring Practices Guide.
In short
- The first rule of ARIA is always to prefer the native HTML element.
- ARIA changes what is announced, not the behaviour: keyboard and focus remain your job.
- Use
aria-labelandaria-labelledbyto name controls without visible text. - Update states such as
aria-expandedon every real change. - Live regions announce updates;
aria-hiddennever goes on focusable elements.