Command Palette

Search for a command to run...

Hectal
PHASE 7Advanced ~27 min· topic 4 of 8

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" or role="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-live or role="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.

Start without ARIAdiagram
Rendering diagram…

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.

index.htmlwhole filehtml
<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".

form.htmlwhole filehtml
<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.

menu.htmlwhole filehtml
<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.

save.htmlwhole filehtml
<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.

price.htmlwhole filehtml
<p>
  <span aria-hidden="true">★★★★☆</span>
  <span class="visually-hidden">Rated 4 out of 5</span>
</p>

Try it yourself

  1. 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. 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. 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

A disclosure button with aria-expanded New tab

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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

aria-expanded: true | panel hidden: false
aria-expanded: false | panel hidden: true
A polite live region New tab

With 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

Press Run to see the page.

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.
Fake button versus real button New tab

`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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

fake focusable: false
real focusable: true

Break 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.

terminal
$ Tab + screen reader
── what you'll see ──
Focus lands on the link, but the screen reader announces nothing (or just "blank")

Break #2

Forget to update aria-expanded

Leave aria-expanded="false" hard-coded while the script shows and hides the menu.

terminal
$ screen reader on the open menu button
── what you'll see ──
"Menu, button, collapsed" while the menu is visibly open

Break #3

Label a div with aria-label

<div aria-label="Product summary">Great mug, 350 ml</div>

terminal
$ screen reader
── what you'll see ──
Reads "Great mug, 350 ml" and ignores the label

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 role overrides 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 as ariaLabelledByElements are 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-live on large regions that change often (every change gets read).

  • ▸

    The HTML <search> element (supported in all major browsers since 2023) maps to the search landmark, replacing <form role="search"> for new code.

Remember this

  1. 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 role attribute and many aria-* 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. 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"> beats role="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 use role="presentation" or aria-hidden="true" on focusable elements, and every interactive element needs an accessible name.

  3. 3

    Roles say what something is. Landmark roles mark page regions (banner, navigation, main, complementary, contentinfo, search, form, region); the HTML elements header, nav, main, aside, footer, search already 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. 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. 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-pressed for 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. 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) or aria-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 and role="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

01

What is the first rule of ARIA, and why does it exist?

02

What's the difference between aria-label, aria-labelledby and aria-describedby?

03

When would you use aria-live="polite" versus role="alert"? What's the common bug with live regions?

04

What does aria-hidden="true" do, and when is it harmful?

Practice

01

Write an icon-only "Delete" button and a toggle button "Bold" that tells assistive tech whether it's on, starting off.

02

Write a search form whose result count ("5 results") is announced politely after each search, without moving focus.

03

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-label is quick, but its text is invisible, easy to forget when the UI changes, and missed by some automatic translation; visible text with aria-labelledby stays 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-label and aria-labelledby, and add hints with aria-describedby.

  • I keep aria-expanded and other states in sync with the UI.

  • I can build a live region that really announces.

  • I never put aria-hidden on focusable content.