In 04-01 we set the golden rule: use the native HTML element before recreating it. In 04-02 we already peeked at attributes like aria-describedby, aria-invalid and role="alert". Now it's time to understand what those attributes are: WAI-ARIA (Accessible Rich Internet Applications), a set of attributes that let you describe semantics —role, state and properties— in the markup when native HTML falls short. ARIA doesn't change what the user sees or add behavior: it only changes what the accessibility tree exposes, the tree we studied in Module 2. It is a layer of information, not of interaction.
For exactly that reason, this lesson treats ARIA as declarative semantics: how to label roles and states in the HTML. Building the interactive widget —capturing the keyboard, moving focus and updating these states with JavaScript— is Module 5 (05-01). Here you'll see, for example, how to give Marta the correct name/role/value of Cursalia's filters dropdown; making it work with the keyboard is the next module.
Contents
- What ARIA is and what it isn't
- The five rules of ARIA
- Roles: landmark, widget and document
- States and properties
- The accessible name:
aria-label,aria-labelledby,aria-describedby aria-hiddenand the danger of hiding focusable content- Introduction to live regions
- Cursalia case: name/role/value of the filters dropdown
- Table of common ARIA attributes
- Common mistakes and tips
- Exercises
- Conclusion
What ARIA is and what it isn't
ARIA contributes three kinds of information to the accessibility tree:
- Roles (
role="..."): what an element is (a button, a tab, an alert). - Properties (
aria-label,aria-describedby...): relatively stable characteristics. - States (
aria-expanded,aria-checked...): conditions that change with interaction.
What ARIA does NOT do, and it's worth burning into memory:
- It doesn't add behavior.
role="button"on a<div>doesn't make it keyboard-clickable or focusable; it only says it's a button. You supply the behavior with JS (Module 5). - It doesn't change appearance. It applies no styles.
- It doesn't improve anything on its own. Badly placed ARIA is worse than none at all, because it lies to the screen reader.
"No ARIA is better than bad ARIA." An incorrect
aria-*creates a broken experience that's hard to diagnose.
The five rules of ARIA
The specification sums up good ARIA use in five rules. They are the compass of this lesson.
- If you can use a native HTML element with the semantics and behavior you need, use it, and don't use ARIA. A
<button>before a<div role="button">. This is the golden rule from 04-01. - Don't change native semantics unless absolutely necessary. Don't do
<h2 role="button">; wrap the text in a<button>inside the<h2>. Changing the native role erases the original (the "heading" disappears). - Every interactive ARIA widget must be keyboard-operable. If you create
role="button", it must receive focus and activate with Enter/Space. (The implementation is Module 5, but the rule is born here.) - Don't put
role="presentation"oraria-hidden="true"on focusable elements. You'd hide from the tree something the keyboard can still reach: the user focuses "nothing". - Every interactive element must have an accessible name. A nameless button ("button, unlabeled") is useless to Marta.
graph TD
A["Do I need extra semantics?"] --> B{"Is there a native<br/>HTML element?"}
B -->|"Yes"| C["Use it. Do NOT use ARIA<br/>(Rule 1)"]
B -->|"No"| D["Use ARIA:<br/>role + states + name"]
D --> E["Make it keyboard-operable<br/>(Rule 3 → Module 5)"]
Roles: landmark, widget and document
ARIA roles are grouped into families. Three matter here:
- Landmark roles: navigation regions. You already get them for free with HTML5 (
<nav>=navigation,<main>=main...). You'd only use the ARIA equivalent (role="navigation") if you can't use the native element. - Widget roles: interactive components (
button,checkbox,tab,menuitem,dialog,slider...). They describe rich controls. Building their behavior is Module 5; here we only declare the role. - Document/structure roles: they organize content (
list,listitem,heading,article,alert,status...).
| Type of HTML5 element | Implicit role (no need to declare it) |
|---|---|
<nav> |
navigation |
<main> |
main |
<button> |
button |
<ul> / <li> |
list / listitem |
<h1>–<h6> |
heading (with its level) |
The lesson: if the native element already has the role, don't repeat it with role. <nav role="navigation"> is redundant.
States and properties
When native HTML can't express a state (because the control doesn't exist natively), ARIA communicates it. The most common in declarative markup:
| Attribute | What it communicates | Typical values |
|---|---|---|
aria-expanded |
Whether a dropdown/accordion is open | true / false |
aria-checked |
State of a custom checkbox/radio | true / false / mixed |
aria-selected |
Selected item (tabs, lists) | true / false |
aria-disabled |
Disabled (but still perceivable by the reader) | true / false |
aria-current |
The current item within a set | page, step, true... |
aria-pressed |
State of a toggle button | true / false |
A declarative example: in Cursalia's navigation bar, mark the current page with aria-current="page" so Marta knows where she is.
<!-- CORRECT: aria-current marks the active page -->
<nav aria-label="Main">
<ul>
<li><a href="/courses" aria-current="page">Catalog</a></li>
<li><a href="/pricing">Pricing</a></li>
</ul>
</nav>Key warning: these states are declarations. aria-expanded="false" on the filters button only says "closed"; updating it to true when it opens is JavaScript's job (Module 5). A state that never updates lies to the reader again.
The accessible name: aria-label, aria-labelledby, aria-describedby
The accessible name is the text the reader announces to identify an element (the name of name/role/value). The browser computes it following a priority. Simplified, from highest to lowest:
aria-labelledby(points to theids of other elements that serve as the name). Wins over everything else.aria-label(literal text in the attribute itself).- The native content/label (the
<button>text, the associated<label for>). - Other mechanisms (
title...), as a last resort.
aria-label: gives a name when there's no visible text. Ideal for the close button (an "X") or the search box with just a magnifying glass.
<!-- CORRECT: with no visible text, aria-label supplies the name -->
<button type="button" aria-label="Close the filters panel">✕</button>aria-labelledby: builds the name from text that's already visible on the page (avoiding duplication and translating twice). It can reference severalids, which are concatenated.
<!-- CORRECT: the region's name is taken from its visible heading -->
<section aria-labelledby="filters-title">
<h2 id="filters-title">Filter courses</h2>
...
</section>aria-describedby: doesn't supply the name but an additional description (read afterward). It's what we used in 04-02 for form hints and errors.
Essential difference: labelledby = what it's called; describedby = extra information. And aria-labelledby overrides the inner text, so use it with care.
aria-hidden and the danger of hiding focusable content
aria-hidden="true" removes an element (and its descendants) from the accessibility tree, without hiding it visually. It's for hiding from the reader things that are purely decorative or duplicated: an icon next to text that already says it.
<!-- CORRECT: the icon is decorative; the text already communicates the action -->
<button type="button">
<span aria-hidden="true">🔍</span> Search
</button>The serious mistake (Rule 4) is putting aria-hidden="true" on something focusable or that contains focusable elements (a link, a button, an input). The result: Lucía tabs to a control the reader considers nonexistent. She focuses "nothing".
<!-- INCORRECT: a link that's still focusable is hidden from the reader -->
<a href="/help" aria-hidden="true">Help</a>If you really want to remove something entirely (visually and from the tree), use hidden, display:none or take it out of the DOM; aria-hidden only affects the tree, not focus.
Introduction to live regions
A live region is an area whose content, when it changes dynamically, the reader announces on its own, without the user having to go to it. It's the basis of criterion 4.1.3 (Status Messages) that we saw as a requirement in Module 3. At the markup level there are three pieces:
aria-live="polite": announces when the reader finishes what it was saying. For non-urgent messages: "Filter applied: 12 courses".aria-live="assertive": interrupts and announces immediately. For the urgent: a submission error.- Shortcut roles:
role="status"is equivalent topolite;role="alert"is equivalent toassertive.
<!-- CORRECT: polite region to confirm filter results -->
<div role="status" aria-live="polite" id="filter-result"></div>// The reader will announce this text as soon as your JS writes it:
document.getElementById('filter-result').textContent =
'Filter applied: 12 courses found.';Important point: the container must exist empty in the DOM from the start; the reader watches for changes within it. We stop at the markup. The detail of dynamic behavior, when to use polite vs assertive in live-validated forms, and testing belong to Modules 5, 6 and 7.
Cursalia case: name/role/value of the filters dropdown
Cursalia has a "Filters" button that shows or hides a panel. Suppose —following rule 1— that we can't use native <details>/<summary> and must build it. Our task in this module is only the correct semantic markup so that Marta perceives what it is and what state it's in:
<!-- Declarative semantics: role (button, native), name (text) and state (aria-expanded) -->
<button type="button"
aria-expanded="false"
aria-controls="filters-panel">
Filters
</button>
<div id="filters-panel" hidden>
<h2>Filter courses</h2>
<!-- category, level, language checkboxes... -->
</div>Let's analyze what Marta receives:
- Role:
button(native, no ARIA needed for the role). - Name: "Filters" (the button text).
- Value/State:
aria-expanded="false"→ the reader announces "collapsed". aria-controls="filters-panel": declares which element the button governs.
With this, Marta hears: "Filters, button collapsed". She knows what it is, what it's called and its state.
What's missing and NOT part of this module: that clicking shows the panel, that aria-expanded becomes true, that hidden is removed, and that the keyboard (Enter/Space/Escape) works. All that behavior is 05-01/05-02. Here we've left the perfect semantic scaffolding so that JavaScript only has to update the state.
Table of common ARIA attributes
| Attribute | Category | What it's for |
|---|---|---|
role |
— | Declares what the element is (if there's no native one) |
aria-label |
Property | Name by literal text (no visible text) |
aria-labelledby |
Property | Name taken from other visible elements |
aria-describedby |
Property | Additional description (hints, errors) |
aria-expanded |
State | Open/closed of dropdowns and accordions |
aria-controls |
Property | Which element this control controls |
aria-current |
State | Current item (page, step) |
aria-selected |
State | Selected item (tabs, options) |
aria-checked |
State | Checked state of a custom checkbox/radio |
aria-disabled |
State | Disabled but perceivable |
aria-hidden |
Property | Hides from the tree (never on focusables!) |
aria-live |
Property | Region announced on change |
Common Mistakes and Tips
- Reinventing with ARIA what already exists.
<div role="button">instead of<button>. Rule 1 always first. - Overriding native semantics.
<a role="button">or<h2 role="tab">erase the original role. It's almost never what you want. - States that don't update. An
aria-expanded="false"that never changes lies. If you set the state, something (JS, Module 5) must keep it current. aria-hiddenon focusable elements. Creates "traps" where focus exists but the reader sees nothing.- Interactive elements with no name. An icon button without
aria-labelis "button, unlabeled". Rule 5. - Confusing
labelledbywithdescribedby. The first is the name; the second, extra information read afterward. - A live region created at the moment of change. It must exist empty from the start so the reader watches it.
- Tip: validate your ARIA with the browser's accessibility tree view (DevTools) and with a real reader. If you're unsure whether to add an
aria-*, the answer is probably to use native HTML.
Exercises
Exercise 1: apply rule 1
A colleague marked up the video lesson's play button like this. Rewrite it complying with the rules of ARIA.
Exercise 2: give an accessible name
The button to close the filters panel only shows an "✕" icon and has no text. Give it an accessible name without adding visible text, and explain which mechanism you use.
Exercise 3: name/role/value of an FAQ accordion
Cursalia has a collapsible FAQ. Write only the semantic markup (role, name and state) for the heading that opens/closes the answer, indicating which part is left for Module 5.
Solutions
Solution 1. Rule 1 says: use the native element. A <button> gives role, focus and keyboard for free, with no tabindex or manual keyboard handlers; the icon is marked decorative and a name is added:
<button type="button" aria-label="Play the lesson" onclick="play()">
<span aria-hidden="true">▶</span>
</button>This satisfies rules 1 (native), 3 (keyboard-operable, for free) and 5 (it has a name).
Solution 2. Since there's no visible text, use aria-label (a property that gives a name by literal text). The icon is hidden from the reader because it's decorative:
<button type="button" aria-label="Close the filters panel">
<span aria-hidden="true">✕</span>
</button>If the name were already written visibly in another element, the preferred option would be aria-labelledby pointing to it.
Solution 3. The accessible pattern wraps the question text in a <button> inside the heading (the <h3>'s role isn't changed; rule 2):
<h3>
<button type="button" aria-expanded="false" aria-controls="answer-1">
Can I pause my subscription?
</button>
</h3>
<div id="answer-1" hidden>
<p>Yes, from your student dashboard you can pause it at any time.</p>
</div>- Role:
button(native) inside aheadingthat is preserved. - Name: the question text.
- State:
aria-expanded="false".
What's left for 05-01: showing/hiding #answer-1, toggling aria-expanded and managing the keyboard. Here we've only left the semantics ready.
Conclusion
ARIA is the way to add semantics —role, state and name— in the markup when native HTML isn't enough, but its first rule is not to use it if there's a native alternative, and badly placed ARIA is worse than none. You've seen the five rules, the types of role, declarative states like aria-expanded, how the accessible name is computed (with the priority labelledby > label > native text), the danger of aria-hidden on focusables and an introduction to live regions. And you've left Cursalia's filters dropdown with an impeccable name/role/value, ready for Module 5 to give it behavior.
We have one piece of the "Perceivable" principle still to implement, and it's pure CSS: making content look and read well. In 04-04 · Color Contrast and Text Resizing we'll close the module with contrast ratios, not relying on color, relative units, reflow and focus visible, applied to Cursalia's brand palette and to Sofía with her zoom at 300%.
Web Accessibility Course
Module 1: Introduction to Web Accessibility
- What Is Web Accessibility?
- The Importance of Web Accessibility
- Overview of Accessibility Laws and Standards
- Introduction to WCAG
Module 2: Understanding Disabilities and Assistive Technologies
Module 3: Principles of Accessible Design
- Perceivable: Making Content Available to the Senses
- Operable: User Interface and Navigation
- Understandable: Information and Operation
- Robust: Compatibility with Current and Future Technologies
Module 4: Implementing Accessibility in HTML and CSS
Module 5: Accessibility in JavaScript and Multimedia
- Creating Accessible JavaScript Widgets
- Keyboard Accessibility
- Accessible Video and Audio Content
- Providing Text Alternatives for Images
Module 6: Accessibility Testing and Evaluation
- Manual Testing Techniques
- Automated Testing Tools
- User Testing with Assistive Technologies
- Interpreting Accessibility Reports
