ARIA Best Practices: When to Use ARIA, When Not To, and How to Get It Right
The rules of ARIA use, how accessible names work, live regions and disclosure widgets, and the common ARIA mistakes we find in audits, with before-and-after code.
Get a manual WCAG auditThe most important of the ARIA best practices is also the simplest: use native HTML first, and reach for ARIA only when HTML can't express what you're building. ARIA (Accessible Rich Internet Applications) is a set of attributes that tell assistive technologies, such as screen readers, what an element is, what state it's in and how it relates to other elements. It changes what assistive technology is told. It doesn't change how anything behaves.
That last point is where most ARIA problems start. Adding role="button" to a <div> makes a screen reader announce "button", but the div still can't be reached with Tab or activated with Enter. The announcement is a promise the code doesn't keep. Used well, ARIA fills real gaps. Used carelessly, it makes a page less accessible than if it had no ARIA at all, which is why the W3C's ARIA Authoring Practices Guide opens with the principle "No ARIA is better than bad ARIA."
What ARIA is: roles, states and properties
ARIA attributes fall into three groups:
| Type | What it describes | Examples |
|---|---|---|
| Roles | What an element is | role="dialog", role="tab", role="status" |
| States | Conditions that change as people interact | aria-expanded, aria-checked, aria-selected |
| Properties | Names, descriptions and relationships | aria-label, aria-labelledby, aria-describedby |
All of it feeds WCAG success criterion 4.1.2 Name, Role, Value (Level A, in WCAG 2.0 and so in the AODA baseline): every user interface component must expose its name and role, and its states must be available to assistive technology and update when they change.
The rules of ARIA use, and the first rule of ARIA
The W3C's Using ARIA document set out four rules that are still the standard reference. W3C has since discontinued work on that document but keeps the rules for reference and points readers to the Authoring Practices Guide for further guidance. Paraphrased:
- The first rule of ARIA: if a native HTML element or attribute already has the semantics and behaviour you need, use it instead of repurposing another element and adding ARIA.
- Don't change native semantics unless you really have to. A heading shouldn't become a tab; put the tab inside the heading or rethink the structure.
- All interactive ARIA controls must be usable with the keyboard. If you give something a widget role, you take on the job of making it work like that widget.
- Don't use
role="presentation"oraria-hidden="true"on a focusable element. Keyboard users would land on something that assistive technology says isn't there.
To these, add a fifth habit that WCAG requires anyway: every interactive element needs an accessible name.
When to use ARIA
Native HTML covers most of a typical website. ARIA earns its place when:
- There's no native element for the widget, such as tabs, a combobox with suggestions, or a tree view.
- A state needs announcing that HTML can't express, such as whether a disclosure button's panel is open (
aria-expanded). - A control needs a name that isn't visible, such as an icon-only button.
- Content updates without a page load and users need to hear about it (live regions).
- Relationships aren't otherwise programmatic, such as hint text that should be read with a field (
aria-describedby).
Before writing any ARIA, check whether HTML already does the job. A <button>, <details> and <summary>, <dialog>, <nav>, a <label> or a required attribute often replaces a whole block of ARIA.
Accessible names: aria-label vs aria-labelledby
An accessible name is what a screen reader announces for an element: "Search, button". Browsers calculate it from several possible sources. In broad terms, aria-labelledby wins first, then aria-label, then native labelling (a <label>, an image's alt, a button's text content), and the title attribute only as a last resort.
aria-labelledbypoints to the ID of visible text already on the page. Prefer it when a visible label exists, because sighted and screen reader users then get the same words.aria-labelsupplies a name as a string that nobody sees. Use it when there's no visible text, such as an icon-only button.
<!-- aria-label: icon-only button with no visible text -->
<button type="button" aria-label="Close">
<svg aria-hidden="true" focusable="false">...</svg>
</button>
<!-- aria-labelledby: reuse a visible heading as the dialog's name -->
<dialog aria-labelledby="delete-title">
<h2 id="delete-title">Delete this report?</h2>
...
</dialog> Both attributes replace the element's own text as its name. That's why aria-label on a button that already has visible text is risky: if the two don't match, voice control users who say what they see may not be able to activate it. WCAG 2.5.3 Label in Name (Level A, WCAG 2.1) requires the visible label text to be part of the accessible name.
<!-- Before: visible text says "View pricing", name says "See plans" -->
<a href="/pricing" aria-label="See plans">View pricing</a>
<!-- After: no aria-label needed; the visible text is the name -->
<a href="/pricing">View pricing</a> For form fields, a real <label> beats both. Our form field accessibility guide covers labels, hints and error messages in detail.
Status messages and aria-live (4.1.3)
When content changes without focus moving, for example "3 items added to cart", "Saved" or "12 results found", screen reader users won't know unless it's announced. WCAG 4.1.3 Status Messages (Level AA, added in WCAG 2.1) requires these messages to be programmatically determinable so assistive technology can announce them without moving focus. Live regions do that:
<!-- In the page from the start, empty -->
<div role="status" id="cart-status"></div>
<script>
// Later, after the item is added:
document.getElementById('cart-status').textContent =
'3 items added to your cart';
</script>role="status"(polite: announced when the screen reader is idle) suits results, confirmations and progress.role="alert"(assertive: interrupts) is for important, time-sensitive warnings and errors only. W3C lists using it for unimportant content as a failure.- Put the live region in the page when it loads and change its text later. Regions inserted with their message already inside are often not announced reliably.
aria-expanded on disclosure buttons
Accordions, "show more" sections and dropdown navigation are disclosure widgets: a button shows or hides a panel. The button needs to be a real button, and it needs to say whether the panel is open.
<!-- Before: not focusable, no role, no state -->
<div class="toggle" onclick="togglePanel()">Shipping options</div>
<div id="shipping-panel" class="hidden">...</div>
<!-- After -->
<button type="button" aria-expanded="false" aria-controls="shipping-panel">
Shipping options
</button>
<div id="shipping-panel" hidden>...</div>
<script>
const button = document.querySelector('[aria-controls="shipping-panel"]');
const panel = document.getElementById('shipping-panel');
button.addEventListener('click', () => {
const isOpen = button.getAttribute('aria-expanded') === 'true';
button.setAttribute('aria-expanded', String(!isOpen));
panel.hidden = isOpen;
});
</script> The hidden attribute also removes the closed panel's links from the tab order, which a purely visual hiding class may not. For a simple show/hide with no custom styling needs, the native <details> and <summary> elements do all of this with no ARIA or script; the FAQ at the end of this page uses them.
Is your ARIA helping or hurting?
Our manual audits test custom components with real screen readers and tell you exactly which attributes to add, change or remove.
See our WCAG auditCommon ARIA mistakes, with fixes
role="button" without keyboard support
A role is a promise. A <div role="button"> must also be focusable and respond to Enter and Space, which means extra script for every one. A native button needs none of it.
<!-- Before -->
<div role="button" onclick="openMenu()">Menu</div>
<!-- After -->
<button type="button" onclick="openMenu()">Menu</button>Our keyboard accessibility guide shows how to test this kind of failure in a few minutes.
aria-hidden on focusable elements
aria-hidden="true" removes an element from the accessibility tree but not from the tab order. Keyboard users then land on something a screen reader can't describe. If you hide a duplicate link, take it out of the tab order as well.
<!-- Before: hidden from screen readers, still reachable with Tab -->
<a href="/insights/aria" aria-hidden="true"><img src="card.jpg" alt=""></a>
<!-- After: a duplicate of the card's title link, removed from both -->
<a href="/insights/aria" aria-hidden="true" tabindex="-1"><img src="card.jpg" alt=""></a> Watch for this on whole regions too: hiding the page behind a modal with aria-hidden while its links remain focusable. The inert attribute handles both at once.
Redundant roles
Native elements already have roles. Repeating them adds nothing, clutters the code and invites mistakes when someone later edits one but not the other.
<!-- Before -->
<nav role="navigation">...</nav>
<button role="button">Save</button>
<div role="heading" aria-level="2">Our services</div>
<!-- After -->
<nav>...</nav>
<button>Save</button>
<h2>Our services</h2>aria-label on non-interactive divs and spans
The ARIA specification prohibits naming elements with the generic role, which is what a plain <div> or <span> has. Screen readers handle such labels inconsistently, so the text is often never heard. Give the element an appropriate role, or put the information in visible text.
<!-- Before: a name on a generic span -->
<span aria-label="Rated 4 out of 5">★★★★☆</span>
<!-- After: an image role can carry a name -->
<span role="img" aria-label="Rated 4 out of 5">★★★★☆</span>States that never update
An aria-expanded="false" that stays false after the menu opens, or an aria-selected left on the first tab, gives screen reader users wrong information. Wrong state is worse than no state. Update ARIA states in the same code that changes the visual state.
Misspelled or invented attributes
Browsers silently ignore ARIA attributes that don't exist. The classic example is aria-labeledby; the correct spelling is aria-labelledby, with two Ls. Automated checkers catch this reliably, which is a good reason to run one.
Application roles on ordinary content
Roles such as menu, menubar, grid and application tell screen readers to expect desktop-application behaviour, with arrow-key navigation that you must then build. A website's main navigation is usually better served by a <nav> containing a list of links, with disclosure buttons for submenus.
Using the ARIA Authoring Practices Guide wisely
The ARIA Authoring Practices Guide (APG) is the best place to learn how a widget such as tabs, a combobox or a dialog is expected to behave, including which keys should do what. Read its caveats before copying code. The APG itself says that testing assistive technology interoperability is essential before using its code in production. Its examples intentionally leave out workarounds for browser and screen reader support gaps, and it doesn't indicate which examples work with mobile browsers or touch. Treat APG patterns as a specification of behaviour, not a drop-in component library, and test what you build.
How to check your ARIA
- Inspect the accessibility tree. Chrome and Edge DevTools show each element's computed name and role in the Accessibility pane; Firefox has an Accessibility Inspector. Check that the name and role are what you intended.
- Run an automated checker, such as our free accessibility scan, to catch invalid attributes, missing names and
aria-hiddenon focusable content. - Test with the keyboard to confirm every ARIA widget actually works.
- Test with a screen reader to hear names, states and live regions. Our screen reader testing guide explains how to get started.
ARIA best practices checklist
- Native HTML elements are used wherever they exist.
- Every element with a widget role is focusable and keyboard operable.
- Every interactive element has an accessible name that includes its visible label.
aria-labelledbyreuses visible text where it exists.- No
aria-hidden="true"on focusable elements or their containers. - No redundant roles on native elements.
- No
aria-labelon plain divs and spans. - States such as
aria-expandedupdate with the visual state. - Status messages use a live region that exists before the message is written.
- Custom widgets follow APG keyboard patterns and have been tested with screen readers.
Frequently asked questions
What is the first rule of ARIA?
If a native HTML element or attribute already provides the semantics and behaviour you need, use it instead of repurposing another element and adding ARIA.
When should I use aria-label instead of aria-labelledby?
Use aria-labelledby when visible text on the page already names the element. Use aria-label only when there is no visible text to point to, such as an icon-only button.
Does adding ARIA make a website more accessible?
Not by itself. ARIA only changes what assistive technology is told. Incorrect ARIA can make a page harder to use than having none, so use it only where native HTML falls short.
Is ARIA required for WCAG or AODA compliance?
No specific ARIA is required. WCAG requires that components expose their name, role and state (4.1.2). Native HTML usually meets that on its own; ARIA is one way to meet it for custom components.
What is the difference between role="status" and role="alert"?
A status is announced politely when the screen reader finishes what it's saying, which suits confirmations and results. An alert interrupts, so reserve it for urgent warnings and errors.