Lesson 17 • Advanced Track
Accessibility Deep Dive & ARIA Roles
By the end of this lesson you'll know when ARIA helps, when it hurts, and how to bolt correct roles, states, and live regions onto custom widgets without breaking screen readers.
What You'll Learn
<button>, <nav> and <label>. If those feel shaky, revisit HTML5 Semantic Architecture first — ARIA builds directly on top of it.💡 Think of It Like This
ARIA is like subtitles on a film. The film (your page) already has visual cues, and subtitles (ARIA) make that same information available to people who can't see the screen. But here's the catch: ARIA only writes the subtitle — it never changes the film. Slapping role="button" on a <div> tells the screen reader "this is a button" but adds zero clicking, focusing, or keyboard behaviour. A real <button> brings the subtitle and the behaviour for free.
The first rule of ARIA: don't use ARIA. If a native HTML element gives you the role and behaviour you need, use it instead.
| Instead of ARIA… | Use native HTML |
|---|---|
<div role="button"> | <button> |
<div role="navigation"> | <nav> |
<span role="heading"> | <h1>–<h6> |
<div role="link"> | <a href> |
<div role="checkbox"> | <input type="checkbox"> |
1. Roles, States & Properties — The Three Jobs of ARIA
ARIA stands for Accessible Rich Internet Applications. It does exactly three things, and it's worth keeping them straight:
- Role — what the thing is:
role="tab",role="alert". - State — a condition that changes as the user interacts:
aria-expanded,aria-checked,aria-hidden. - Property — a fixed characteristic that rarely changes:
aria-label,aria-haspopup.
The example below puts all three on one custom toggle so you can see how they read out. Notice the comments stating exactly what a screen reader announces.
Roles, States & Properties Together
One toggle showing a role, a changing state, and a fixed property
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Role, State, Property</title>
<style>
body { background:#0f172a; color:#e5e7eb; font-family:system-ui, sans-serif; padding:30px; }
.switch {
background:#334155; color:#fff; border:none; padding:12px 18px;
border-radius:6px; cursor:pointer; font-size:15px;
}
.switch[aria-pressed="true"] { background:#22c55e; color:#0f172a; }
.note { color:#94a3b8; font-size:13px; margin-top:10px; }
code
...2. Naming Elements: aria-label, aria-labelledby & aria-describedby
Every interactive element needs an accessible name — the text a screen reader speaks. Three attributes give you one:
aria-label— type the name directly. Use it when there's no visible text (an icon button).aria-labelledby— point at theidof existing visible text, so the name stays in sync.aria-describedby— point at extra help text spoken after the name (a hint, not the name itself).
ARIA Labelling Techniques
Give every control a clear accessible name
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ARIA Labels</title>
<style>
body { background:#0f172a; color:#e5e7eb; font-family:system-ui, sans-serif; padding:30px; }
.example { background:#1e293b; padding:20px; border-radius:8px; margin:15px 0; }
h3 { color:#22c55e; margin:20px 0 8px; font-size:1rem; }
button { background:#3b82f6; color:#fff; border:none; padding:10px; border-radius:6px;
cursor:pointer; width:40px; height:40px; font-siz
...3. Common States: aria-expanded, aria-hidden & aria-current
States communicate what's happening right now. The three you'll reach for most:
aria-expanded— is this menu/accordion open? Flip it with JavaScript on each toggle.aria-hidden="true"— hide decorative content from screen readers. Never put it on anything focusable.aria-current="page"— mark the current item in a nav, pagination, or breadcrumb.
States in Action
A disclosure button, a decorative icon, and a current nav item
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ARIA States</title>
<style>
body { background:#0f172a; color:#e5e7eb; font-family:system-ui, sans-serif; padding:30px; }
.disclosure { background:#334155; color:#fff; border:none; padding:10px 16px;
border-radius:6px; cursor:pointer; font-size:15px; }
.panel { background:#1e293b; padding:14px; border-radius:6px; margin-top:8px; }
nav a { color:#94a3b8; text-decoration:none; margin-right:
...4. Live Regions — Announcing Dynamic Content
When content changes without a page reload — a "saved" toast, a form error, a cart count — sighted users see it, but a screen reader stays silent unless you mark the container as a live region. Use aria-live="polite" for routine updates and aria-live="assertive" (or role="alert") for urgent errors that should interrupt.
ARIA Live Regions
Polite updates, assertive alerts, and role=status
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Live Regions</title>
<style>
body { background:#0f172a; color:#e5e7eb; font-family:system-ui, sans-serif; padding:30px; }
button { background:#3b82f6; color:#fff; border:none; padding:10px 18px; border-radius:6px;
cursor:pointer; font-size:14px; margin:5px 0; }
.status { background:#14532d; border:1px solid #22c55e; padding:12px; border-radius:6px; margin-top:10px; }
.alert { background:#7f1
...5. Roles for Custom Widgets: Tabs & Menus
Some widgets — tabs, menus, sliders — have no native HTML element. Here ARIA earns its keep: you build the structure from <div>s, then describe it with a coordinated set of roles and states (a "design pattern"). A tab set needs role="tablist", role="tab" with aria-selected, and role="tabpanel".
Accessible Tabs
A tab widget wired up with the correct roles and states
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Accessible Tabs</title>
<style>
body { background:#0f172a; color:#e5e7eb; font-family:system-ui, sans-serif; padding:30px; }
[role="tablist"] { display:flex; gap:4px; border-bottom:1px solid #334155; }
[role="tab"] { background:none; color:#94a3b8; border:none; padding:10px 16px;
cursor:pointer; font-size:15px; }
[role="tab"][aria-selected="true"] { color:#22c55e; border-bottom:2px soli
...role="menu" / role="menuitem") also needs arrow-key navigation. That's a lot of JavaScript — which is exactly why a plain <nav> of <a> links is better whenever it fits.🎯 Your Turn #1 — Name the Icon Buttons
These icon-only buttons are silent to a screen reader. Add an accessible name to each by filling in the blanks. One concept, one minute.
🎯 Your Turn: Add Accessible Names
Fill in the aria-label blanks marked with ___
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Your Turn — Labels</title>
<style>
body { background:#0f172a; color:#e5e7eb; font-family:system-ui, sans-serif; padding:30px; }
button { background:#3b82f6; color:#fff; border:none; width:44px; height:44px;
border-radius:6px; cursor:pointer; font-size:20px; margin:6px; }
</style>
</head>
<body>
<h1>Toolbar</h1>
<!-- 🎯 YOUR TURN — replace each ___ with a clear aria-label -->
<!-- 1) This tr
...🎯 Your Turn #2 — Wire Up aria-expanded
This "Read more" disclosure shows and hides a panel, but the screen reader never learns whether it's open. Add the missing state and keep it in sync.
🎯 Your Turn: Announce Open/Closed
Add aria-expanded and update it in the blanks marked with ___
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Your Turn — Expanded</title>
<style>
body { background:#0f172a; color:#e5e7eb; font-family:system-ui, sans-serif; padding:30px; }
button { background:#334155; color:#fff; border:none; padding:10px 16px;
border-radius:6px; cursor:pointer; font-size:15px; }
.panel { background:#1e293b; padding:14px; border-radius:6px; margin-top:8px; }
</style>
</head>
<body>
<h1>FAQ</h1>
<!-- 🎯 YOUR TURN —
...🧩 Mini-Challenge — Accessible "Saved" Toast
Support is faded now — only an outline is given. Build a button that, when clicked, makes a live region announce that the form was saved. Use what you learned in Section 4.
🧩 Mini-Challenge: Live Toast
Only a comment outline is provided — you write the code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Mini-Challenge — Toast</title>
<style>
body { background:#0f172a; color:#e5e7eb; font-family:system-ui, sans-serif; padding:30px; }
button { background:#22c55e; color:#0f172a; border:none; padding:10px 18px;
border-radius:6px; cursor:pointer; font-weight:600; }
.toast { background:#14532d; border:1px solid #22c55e; padding:12px;
border-radius:6px; margin-top:12px; min-height:20px; }
...⚠️ Common Errors (and the Fix)
- Redundant ARIA on semantic elements.
<button role="button">or<nav role="navigation">repeat a role the element already has — and some redundant roles actively suppress native behaviour. Fix: drop the role; the native element already carries it. - aria-hidden on a focusable element. Putting
aria-hidden="true"on (or around) a link or button creates a "ghost" stop — keyboard users Tab to it but screen readers say nothing. Fix: never hide focusable content; if it must be hidden, remove it from the tab order too (e.g. with thehiddenattribute ortabindex="-1"plus removal). - Wrong or invented roles.
role="text"on a heading, orrole="button"on something that scrolls, lies to the user about what the element does. Fix: use a real role from the ARIA spec that matches the actual behaviour — or a native element instead. - aria-label ignored on a non-interactive element. Browsers often drop
aria-labelon a plain<span>or<div>with no role. Fix: put labels on elements that take a name (buttons, links, inputs, or things with a widget/landmark role).
📋 ARIA Quick Reference
| Attribute / role | Kind | What it does |
|---|---|---|
aria-label | Property | Sets the accessible name directly (no visible text) |
aria-labelledby | Property | Names an element from another element's id |
aria-describedby | Property | Adds help text spoken after the name |
aria-expanded | State | Whether a disclosure / menu is open |
aria-hidden | State | Hides decorative content from screen readers |
aria-current | State | Marks the current item (page, step, date) |
aria-live | Property | "polite" or "assertive" — announce changes |
role="alert" | Role | Assertive live region for urgent errors |
role="tablist"/"tab"/"tabpanel" | Role | Structure for a custom tab widget |
💡 Pro tip: before adding any of these, ask "is there a native element that already does this?" — the answer is usually yes.
❓ Frequently Asked Questions
What is the difference between a role, a state, and a property in ARIA?
A role tells assistive tech what a thing IS (role="tab", role="alert"). A state describes a condition that changes as the user interacts (aria-expanded, aria-checked, aria-hidden). A property describes a fixed characteristic that rarely changes (aria-label, aria-labelledby, aria-haspopup). Roles and properties are usually set once; states are updated with JavaScript.
Why is 'the first rule of ARIA: don't use ARIA' the most important rule?
Native HTML elements like <button>, <nav>, <a href> and <input> already carry the correct role, keyboard behaviour, and focus handling for free. ARIA changes only how a screen reader describes an element — it adds zero behaviour. So a <div role="button"> still needs you to wire up Tab focus, Enter/Space keys, and a focus ring by hand, and most people forget at least one. Reaching for the native element is less code and fewer bugs.
When should I use aria-label versus aria-labelledby?
Use aria-label when there is NO visible text to name the element — for example an icon-only button (aria-label="Close"). Use aria-labelledby when the name already exists as visible text somewhere on the page; you point it at that element's id so the name stays in sync. If both are present, aria-labelledby wins.
What does aria-hidden="true" do, and what is the trap?
aria-hidden="true" removes an element and all its children from the accessibility tree, so screen readers skip it — useful for decorative icons. The trap: it does NOT remove the element from keyboard focus. If you hide a container that holds a focusable button, a keyboard user can still Tab to a button that screen readers cannot announce — a confusing dead spot. Never put aria-hidden on, or around, anything focusable.
What is an aria-live region and when do I need one?
A live region is an element with aria-live (or role="status"/role="alert") that tells the screen reader to announce changes to its content automatically. You need one whenever content updates without a page reload — form errors, search-result counts, 'saved' toasts. Use aria-live="polite" for non-urgent updates and aria-live="assertive" (or role="alert") for errors that must interrupt.
Do I still need to handle the keyboard if I add a role to a div?
Yes. ARIA roles only change the spoken label — they add no behaviour. A <div role="button"> needs tabindex="0" to be focusable and JavaScript to respond to Enter and Space. This is exactly why native <button> is preferred: it does all of that automatically.
🎉 Lesson Complete
You can now add ARIA the way pros do — sparingly and correctly:
- ✅ Native HTML first — ARIA changes the label, never the behaviour
- ✅ Name controls with
aria-label/aria-labelledby, add hints witharia-describedby - ✅ Track conditions with
aria-expanded,aria-hiddenandaria-current - ✅ Announce dynamic updates through
aria-liveregions - ✅ Reach for widget roles (tabs, menus) only when no native element fits
Up next: Complex Forms & Validation — where good labelling and live regions make error handling genuinely usable.
Sign up for free to track which lessons you've completed and get learning reminders.