Topic 7.4
ARIA Basics
In one line
ARIA attributes add roles, names and states to the accessibility tree when HTML has no element for the job; they change only what assistive tech hears, never behaviour, so native HTML always comes first.
Think of it like this
Name badges at a conference. A badge saying "Speaker" tells people who you are, but it doesn't give you the ability to speak well. If you wear a "Doctor" badge without being a doctor, people will trust you and get hurt. ARIA is a badge: it tells assistive tech what an element is, but it doesn't make the element behave like that thing.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- ARIA
- Accessible Rich Internet Applications: attributes that add roles, names and states for assistive tech.
- role
- The attribute that says what an element is, like
role="dialog"orrole="status". - Landmark
- A major page region (header, nav, main, aside, footer, search) that screen reader users can jump between.
- aria-label
- Gives an element a name written as a string, when there's no visible text to use.
- aria-labelledby
- Gives an element a name taken from the text of other elements, listed by id.
- aria-describedby
- Adds extra description, like a hint or error, from other elements' text.
- aria-expanded
- Says whether the thing a button controls is open (
true) or closed (false). - aria-hidden
- Removes an element and everything inside it from the accessibility tree; it still shows on screen.
- Live region
- A container whose text changes are read out automatically, set with
aria-liveorrole="status"/"alert".
Step by step
01Start without ARIA
Before reaching for ARIA, ask: is there an HTML element for this? Buttons, links, checkboxes, radios, selects, dialogs (<dialog>), disclosure widgets (<details>), progress bars (<progress>), meters, tables and landmarks are all native. Native elements come with roles, states, keyboard support and focus already done.
02Name things that have no text
An icon-only button and a second navigation landmark both need names. aria-label writes the name; aria-labelledby points at visible text.
<button type="button" aria-label="Open menu">☰</button>
<nav aria-label="Main">…</nav>
<nav aria-labelledby="crumbs-title">
<h2 id="crumbs-title">You are here</h2>
…
</nav>03Describe extra information
A description is read after the name and role, so the user hears "Password, edit text, at least 12 characters".
<label for="pw">Password</label>
<input id="pw" type="password" aria-describedby="pw-hint">
<p id="pw-hint">At least 12 characters.</p>04Expose state and keep it in sync
A button that opens a menu tells assistive tech whether the menu is open. Your script flips aria-expanded every time it shows or hides the menu. If the attribute and the screen disagree, the user is misinformed.
<button type="button" aria-expanded="false" aria-controls="menu">Menu</button>
<ul id="menu" hidden>…</ul>
<script>
const btn = document.querySelector("[aria-controls=menu]");
btn.addEventListener("click", () => {
const open = btn.getAttribute("aria-expanded") === "true";
btn.setAttribute("aria-expanded", String(!open));
document.getElementById("menu").hidden = open;
});
</script>05Announce changes with a live region
Place an empty status container in the HTML from the start. Later, setting its text makes the screen reader say it once, without moving focus.
<button type="button" id="save">Save</button>
<p role="status" id="msg"></p>
<script>
document.getElementById("save").addEventListener("click", () => {
document.getElementById("msg").textContent = "Draft saved.";
});
</script>06Hide duplicates, not content
aria-hidden="true" is for things that would be read twice or are pure decoration. Never put it on something focusable, or on content sighted users rely on: a keyboard user would land on an element the screen reader says nothing about.
<p>
<span aria-hidden="true">★★★★☆</span>
<span class="visually-hidden">Rated 4 out of 5</span>
</p>Try it yourself
- 1
Let CSS follow ARIA
In the first example, add a rule
button[aria-expanded="true"] { background: #dbeafe; }. Predict when the button turns blue, then click it in the preview. - 2
Remove the live region role
In the second example, delete
role="status". The page looks identical and the log is the same, but a screen reader would now say nothing when the text changes. Visible updates aren't automatically announced. - 3
Repair the fake button
In the third example, add
tabindex="0"to the div. Predict the new output. It becomes focusable, but click in the preview, Tab to it and press Enter: no "activated" line. You'd also need a keydown handler for Enter and Space. Then compare with the real button.
Code & diagrams
The CSS styles the arrow from the ARIA state, so the visual and the announced state can never drift apart. (For a simple show/hide, `<details>` gives you all of this with no JavaScript.)
Preview
Console
Nothing logged yet.Expected console output
aria-expanded: true | panel hidden: false
aria-expanded: false | panel hidden: trueWith a screen reader running, each change is read out without focus leaving the button. The region was in the page, empty, from the start.
Preview
Console
Nothing logged yet.Expected console output
role: status | exists before any message: true
announced: 1 item in your cart.
announced: 2 items in your cart.`role="button"` changed only the announcement. The div still can't be focused, so a keyboard user can never press it. Making it work needs `tabindex="0"` plus Enter and Space key handlers: all things `<button>` already does.
Preview
Console
Nothing logged yet.Expected console output
fake focusable: false
real focusable: trueBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Hide a focusable element with aria-hidden
Put aria-hidden="true" on a container that has a link inside it.
Break #2
Forget to update aria-expanded
Leave aria-expanded="false" hard-coded while the script shows and hides the menu.
Break #3
Label a div with aria-label
<div aria-label="Product summary">Great mug, 350 ml</div>
Myth vs fact
Myth
Adding ARIA makes a page more accessible.
Fact
Wrong or unnecessary ARIA makes it worse. The WebAIM Million study finds that home pages using ARIA have more detected errors on average than pages without it. Native HTML first.
Myth
role="button" makes a div work like a button.
Fact
It changes only what's announced. Focus, Enter, Space and form submission behaviour must all be added by hand.
Myth
aria-hidden="true" hides an element on screen.
Fact
It hides it only from assistive tech. To hide from everyone, use hidden or display: none.
Myth
Use role="alert" for every message so nobody misses it.
Fact
Alerts interrupt whatever the user is hearing. Use role="status" (polite) for normal updates and keep alerts for urgent, time-sensitive problems.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Browsers map ARIA to platform accessibility APIs as defined in the Core Accessibility API Mappings and HTML-AAM specs. Explicit
roleoverrides the native role in the tree, which is why<h2 role="button">stops being a heading. - ▸
ARIA reflection lets JavaScript read and set ARIA as properties:
el.ariaExpanded = "true",el.role = "status". It's supported in all major engines (Firefox added it in version 119, 2023). Element references such asariaLabelledByElementsare newer and can cross shadow DOM boundaries, which id references can't. - ▸
Live regions are notoriously inconsistent across screen readers. Practical rules: create the region at page load, keep it empty, change text with a short delay after any DOM insertion, keep messages short, and don't put
aria-liveon large regions that change often (every change gets read). - ▸
The HTML
<search>element (supported in all major browsers since 2023) maps to thesearchlandmark, replacing<form role="search">for new code.
Remember this
- 1
ARIA stands for Accessible Rich Internet Applications, a W3C standard (current version WAI-ARIA 1.2). It is a set of HTML attributes: one
roleattribute and manyaria-*attributes. They change the accessibility tree only: the role, name, description and state that assistive tech receives. They don't change how the element looks, whether it's focusable, or what keys do. - 2
The first rule of ARIA (from W3C's "Using ARIA" note): if a native HTML element or attribute already has the meaning and behaviour you need, use it instead of adding ARIA.
<button>beats<div role="button">;<nav>beats<div role="navigation">;<input type="checkbox">beatsrole="checkbox". The other rules: don't change native semantics unless you really must (no<h2 role="button">), every interactive ARIA control must work with the keyboard, don't userole="presentation"oraria-hidden="true"on focusable elements, and every interactive element needs an accessible name. - 3
Roles say what something is. Landmark roles mark page regions (
banner,navigation,main,complementary,contentinfo,search,form,region); the HTML elementsheader,nav,main,aside,footer,searchalready have them. Widget roles describe controls (button,checkbox,tab,tabpanel,dialog,switch,slider,menu). Document structure roles describe content (heading,list,img,table). Live region roles (status,alert,log) announce changes. - 4
Naming and describing.
aria-label="Close"gives an element a name as a string (used when there's no visible text).aria-labelledby="id1 id2"names it from the text of other elements, by id (preferred when the name is visible on screen).aria-describedby="hint-id"adds a description, extra information read after the name and role, such as a hint or an error message. Topic 7.5 explains exactly which wins when several are present. - 5
States say what condition something is in, and your JavaScript must keep them true.
aria-expanded="true|false"on a button that shows and hides something (a menu, an accordion).aria-pressedfor toggle buttons.aria-current="page"on the nav link for the current page.aria-controls="id"points to the element the button controls (support in screen readers is weak, so it's a hint, not a requirement).aria-hidden="true"removes an element and all its children from the accessibility tree, though it stays visible on screen: for decorative icons and duplicated content. - 6
Live regions make screen readers announce changes that happen without focus moving, like "Saved" or "3 results". Put
aria-live="polite"(wait until the user pauses) oraria-live="assertive"(interrupt now; use rarely) on a container that is already in the page, then change its text.role="status"is a polite live region androle="alert"is an assertive one. The<output>element is a native status region. A live region added at the same moment as its text is often not announced, because there was nothing to watch yet.
Explain it without notes
What is the first rule of ARIA, and why does it exist?
What's the difference between aria-label, aria-labelledby and aria-describedby?
When would you use aria-live="polite" versus role="alert"? What's the common bug with live regions?
What does aria-hidden="true" do, and when is it harmful?
Practice
Write an icon-only "Delete" button and a toggle button "Bold" that tells assistive tech whether it's on, starting off.
Write a search form whose result count ("5 results") is announced politely after each search, without moving focus.
Mark up a site navigation where the link to the current page is identified to assistive tech.
Trade-offs
- ↔
Custom ARIA widgets (tabs, comboboxes, menus) give design freedom but you own every key, state and screen reader quirk; prefer native elements or a well-tested component library that follows the ARIA Authoring Practices.
- ↔
aria-labelis quick, but its text is invisible, easy to forget when the UI changes, and missed by some automatic translation; visible text witharia-labelledbystays in sync.
Done when you can
I can state the first rule of ARIA and pick a native element first.
I can name things with
aria-labelandaria-labelledby, and add hints witharia-describedby.I keep
aria-expandedand other states in sync with the UI.I can build a live region that really announces.
I never put
aria-hiddenon focusable content.