ARIA Basics & the First Rule
WAI-ARIA, usually just ARIA (Accessible Rich Internet Applications), is a W3C standard: a set of HTML attributes that add or change information in the accessibility tree. With ARIA you can tell assistive technology that a <div> is a tab, that a panel is expanded, or that a message should be read aloud. The current version is WAI-ARIA 1.2, a W3C Recommendation since June 2023; version 1.3 is still a draft.
ARIA is powerful, and that is exactly why it is dangerous. In this lesson you will learn what ARIA changes and what it does not, the famous first rule of ARIA, how a control's accessible name is worked out, and the mistakes that make pages worse than having no ARIA at all.
What ARIA does and does not do
ARIA attributes come in three groups:
- Roles say what an element is:
role="tab",role="dialog",role="alert". - States describe conditions that change:
aria-expanded="true",aria-pressed,aria-checked,aria-invalid. - Properties describe characteristics and relationships that rarely change:
aria-label,aria-labelledby,aria-describedby,aria-controls,aria-required.
ARIA only changes what assistive technology is told. It does not:
- make an element focusable,
- add keyboard behaviour,
- change how it looks,
- make the browser do anything when it is clicked.
So <div role="button"> is announced as "button", but it still cannot be reached with Tab or pressed with Enter or Space until you add tabindex="0" and keyboard code yourself. The W3C's ARIA Authoring Practices Guide puts it simply: no ARIA is better than bad ARIA, and a role is a promise that you will build the behaviour that goes with it.
The first rule of ARIA
The W3C's guidance on using ARIA lists a few rules. The first one is the most important:
If a native HTML element or attribute already has the meaning and behaviour you need, use it instead of adding ARIA to another element.
| Instead of | Use |
|---|---|
<div role="button" tabindex="0"> | <button type="button"> |
<span role="link"> | <a href="..."> |
<div role="checkbox" aria-checked="false"> | <input type="checkbox"> |
<div role="heading" aria-level="2"> | <h2> |
<div role="navigation"> | <nav> |
<div aria-required="true"> field | <input required> |
The other rules follow from it: do not change the meaning of a native element unless you really must (for example, do not put role="heading" on a <button>); every interactive ARIA control must work with the keyboard; never hide a focusable element with aria-hidden="true"; and every interactive element needs an accessible name.
Bad ARIA in action
This example contains three common ARIA mistakes. Run it and use the Tab key, then read the explanation below it.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Bad ARIA</title>
<style>
.btn { display: inline-block; padding: 6px 12px; background: #0b5cad; color: #fff; cursor: pointer; }
</style>
</head>
<body>
<h1>Bus pass renewal</h1>
<p>1. <span class="btn" role="button" id="renew">Renew pass</span></p>
<p>2. <a href="#help" aria-hidden="true">Help with renewal</a></p>
<p>3. <button type="button" aria-label="Submit form 7B">Renew now</button></p>
<p id="msg"></p>
<script>
document.querySelector("#renew").addEventListener("click", () => {
document.querySelector("#msg").textContent = "Renewal started.";
});
</script>
</body>
</html>- The
<span>hasrole="button", so a screen reader calls it a button, but Tab skips it and Enter does nothing. The role made a promise the code does not keep. - The link has
aria-hidden="true". It is removed from the accessibility tree, yet it can still be focused with Tab. A screen reader user lands on "nothing" and hears silence or something confusing. - The button shows "Renew now" but its
aria-labelreplaces that with "Submit form 7B". A voice control user who says "click Renew now" gets no response, which fails 2.5.3 Label in Name.
Each problem is fixed by removing ARIA, not adding more: use a <button>, remove aria-hidden, and delete the aria-label.
How the accessible name is calculated
The accessible name is what a screen reader announces as the name of a control. The browser computes it from several possible sources, roughly in this order of priority:
aria-labelledby: the text of the element or elements whose ids it lists.aria-label: a text string.- The native label:
<label>for form fields,altfor images,<caption>for tables,<legend>for fieldsets. - The element's own content, for roles such as button, link and heading.
- The
titleattribute, as a last resort.
The first source found wins. This explains why aria-label on a button hides the button's visible text from screen readers.
<!-- Name: "Delete" (content) -->
<button type="button">Delete</button>
<!-- Name: "Delete Kochi trip" (aria-labelledby, combining two elements) -->
<button type="button" id="del-1" aria-labelledby="del-1 trip-1">Delete</button>
<span id="trip-1">Kochi trip</span>
<!-- Name: "Close" (aria-label; the button has no visible text) -->
<button type="button" aria-label="Close">✕</button>Good ARIA: a disclosure button
ARIA is the right tool when HTML has no attribute for the information you need. A button that shows and hides a panel is a good example: HTML can make the button, but only ARIA can say whether the panel is currently open.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Disclosure Button</title>
</head>
<body>
<h1>Pune Metro fares</h1>
<button type="button" id="toggle" aria-expanded="false" aria-controls="fare-details">
Show fare details
</button>
<div id="fare-details" hidden>
<p>Fares are worked out by distance. Check the official metro website for current fares.</p>
</div>
<script>
const toggle = document.querySelector("#toggle");
const panel = document.querySelector("#fare-details");
toggle.addEventListener("click", () => {
const open = toggle.getAttribute("aria-expanded") === "true";
toggle.setAttribute("aria-expanded", String(!open));
panel.hidden = open;
});
</script>
</body>
</html>The native <button> gives role, focus and keyboard support. ARIA adds only the missing piece: aria-expanded tells screen readers "collapsed" or "expanded" every time the state changes. For simple show-and-hide content, <details> and <summary> do this without any script, as you will see in Dialogs & Accordions.
Common mistakes
- ARIA instead of HTML.
role="button"on a<div>,role="link"on a<span>, and so on. - Roles without behaviour. Every role you add must come with the keyboard support users expect.
aria-hidden="true"on focusable elements, or on a parent of them.aria-labelthat disagrees with visible text. Keep the visible words in the name.- Stale states.
aria-expanded="false"on a menu that is visibly open is worse than no attribute. - Copying ARIA from old code without understanding it.
Key points
- ARIA adds roles, states and properties to the accessibility tree, and nothing else.
- The first rule of ARIA: if native HTML can do it, use native HTML.
- A role is a promise: you must add the matching keyboard behaviour.
- The accessible name comes from
aria-labelledby, thenaria-label, then native labels, then content, thentitle. - Use ARIA for information HTML cannot express, such as
aria-expanded, and keep states up to date.
Exercise
Fix this snippet so it follows the first rule of ARIA. Remove every ARIA attribute that native HTML makes unnecessary, and keep only ARIA that adds real information.
<div role="navigation" aria-label="Main">
<span role="link" onclick="go('/')">Home</span>
</div>
<div role="button" aria-expanded="false" onclick="toggleFilters()">Filters</div>
<div role="checkbox" aria-checked="true">Show only AC coaches</div>Show solution
<nav aria-label="Main">
<a href="/">Home</a>
</nav>
<button type="button" id="filters-btn" aria-expanded="false" aria-controls="filters">Filters</button>
<input type="checkbox" id="ac-only" checked>
<label for="ac-only">Show only AC coaches</label><nav>, <a href>, <button> and <input type="checkbox"> replace four roles. aria-label stays on the navigation to name the landmark, and aria-expanded stays because HTML has no native way to say whether the filters panel is open.
Quick Quiz
1. What is the first rule of ARIA?
Native HTML gives meaning and behaviour together; ARIA only gives meaning.
2. What does role="button" on a <div> add?
ARIA changes only what assistive technology is told, so you must add tabindex and key handling yourself.
3. A button has visible text "Pay" and aria-labelledby pointing at a heading "Cart total". What is its accessible name?
aria-labelledby has the highest priority and replaces the button's content as the name.
4. Why is aria-hidden="true" on a focusable link a problem?
Never hide focusable elements from the accessibility tree; remove them from focus too, or do not hide them.
Learn Web Accessibility with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
