01 · Accessibility Foundations & the Accessibility Tree¶
Accessibility means people can use what you build regardless of how they perceive and operate it: with a screen reader, a keyboard only, voice commands, a switch device, magnification at 400%, high-contrast colours, captions, or just with one hand holding a baby. The World Health Organization estimates that about one in six people worldwide live with a significant disability — and nearly everyone has temporary or situational limitations (a broken wrist, bright sunlight, a noisy train).
For HTML and CSS authors, accessibility is mostly not an extra layer. It's a consequence of the choices from Levels 1 and 2 — semantic elements, labels, contrast, focus styles, reflow at zoom. This level makes those choices deliberate, explains the machinery underneath, and covers the cases where native HTML isn't enough.
Who uses what¶
| People | Often use | What breaks for them |
|---|---|---|
| Blind and low-vision | Screen readers (NVDA, JAWS, VoiceOver, TalkBack, Narrator), magnifiers, zoom | Missing alt text, unlabelled controls, meaningless structure, text that doesn't reflow |
| Motor impairments | Keyboard, switch access, voice control (Voice Control, Dragon), eye tracking | Mouse-only interactions, tiny targets, no visible focus, time limits |
| Deaf and hard of hearing | Captions, transcripts | Uncaptioned video, audio-only alerts |
| Cognitive and learning | Consistent layouts, plain language, reader modes | Clutter, unpredictable changes, jargon, animations |
| Vestibular disorders | Reduced-motion settings | Parallax, large animations |
| Colour vision deficiency | Nothing special — they just see differently | Information conveyed by colour alone |
WCAG in one page¶
The Web Content Accessibility Guidelines (WCAG) are the international standard, and the basis of most accessibility laws. Version 2.2 is current; each version is backwards compatible. Success criteria are organised under four principles — POUR:
- Perceivable — text alternatives, captions, content that adapts (reflow, zoom), sufficient contrast.
- Operable — everything works from a keyboard, visible focus, enough time, no seizure-inducing flashing, targets big enough to hit.
- Understandable — readable text, predictable behaviour, help avoiding and fixing errors.
- Robust — valid, well-structured markup that assistive technologies can interpret.
Each criterion has a level: A (the minimum), AA (the usual legal and contractual target), AAA (enhanced). "WCAG 2.2 AA" is the target to design for. The criteria themselves are testable statements, like 1.4.3 Contrast (Minimum): text has a contrast ratio of at least 4.5:1.
The accessibility tree¶
Assistive technologies don't read your HTML or look at your pixels. The browser builds a second tree alongside the DOM — the accessibility tree — and exposes it through the operating system's accessibility API (UI Automation on Windows, NSAccessibility on macOS, AT-SPI on Linux, and the mobile equivalents). Every node has:
- a role — what it is:
button,link,heading,checkbox,navigation... - a name — what it's called: "Close dialog", "Search recipes".
- states and properties — checked, expanded, disabled, invalid, level 2...
- sometimes a value and a description.
A screen reader announces those: "Search recipes, search text field", "Vegetarian, checkbox, checked". Voice control uses the names: the user says "click Close dialog".
You can see the tree in devtools (Chrome/Edge: Elements → Accessibility pane, with "full
accessibility tree" enabled; Firefox: the Accessibility panel; Safari: Elements → Node →
Accessibility). Throughout this level we used Playwright's ariaSnapshot(), which prints
the role and name of each node as YAML.
Worked example: where names come from¶
We rendered a set of common elements in Chromium and printed the accessibility tree:
<button><svg aria-hidden="true">…</svg></button>
<button aria-label="Close dialog"><svg aria-hidden="true">…</svg></button>
<button aria-label="Ignored label" aria-labelledby="t">x</button><span id="t">Delete draft</span>
<img src="a.png" alt="Company logo">
<img src="b.png" alt="">
<img src="c.png">
<label for="q">Search recipes</label><input id="q" type="search">
<input type="text" placeholder="Only a placeholder">
<input type="text" title="Only a title">
<a href="/x"><img src="d.png" alt="Home"></a>
<button>Save <span class="visually-hidden">recipe</span></button>
- button
- button "Close dialog"
- button "Delete draft": x
- img "Company logo"
- img
- searchbox "Search recipes"
- textbox "Only a placeholder"
- textbox "Only a title"
- link "Home":
- img "Home"
- button "Save recipe"
Line by line:
- An icon-only button with no text has no name. A screen reader says "button" and nothing else. This is one of the most common failures on the web.
aria-labelprovides a name where there's no visible text.aria-labelledbybeatsaria-label— the name is "Delete draft", taken from the referenced element. Neither appears visually on the button; the button's own textxis ignored for naming.altnames an image.alt=""removes the image from the tree completely — correct for decoration.- No
altat all leaves an unnamedimgin the tree. Screen readers then often announce the file name. - A
<label>names its input. - A
placeholderis used as a last-resort name when nothing else exists, and so istitle. They're not reliable substitutes for a label: placeholders vanish when you type, andtitleisn't shown on touch devices. - A link's name comes from its content — here the image's alt.
- Visually hidden text (explained below) still contributes to the name: "Save recipe".
This follows the accessible name computation algorithm, which checks sources in
priority order: aria-labelledby, then aria-label, then native labelling (<label>,
alt, <caption>, <legend>...), then the element's content (for roles that allow it,
like buttons and links), then title or placeholder.
Hiding things: four techniques, four meanings¶
We rendered four paragraphs and checked which appeared in the accessibility tree:
<p style="display:none">display none</p>
<p style="visibility:hidden">visibility hidden</p>
<p aria-hidden="true">aria hidden</p>
<p style="opacity:0">opacity zero</p>
| Technique | Visible? | In accessibility tree? | Focusable? |
|---|---|---|---|
display: none, hidden attribute |
no | no | no |
visibility: hidden |
no (space kept) | no | no |
aria-hidden="true" |
yes | no | yes — danger |
opacity: 0 |
no | yes | yes |
| visually-hidden class | no | yes | yes |
The two "danger" cases are mirror images. aria-hidden on something focusable creates a
control a keyboard user can reach but a screen reader can't describe. opacity: 0 leaves
invisible content that screen readers still announce.
The visually-hidden pattern hides content from sight but keeps it for assistive tech — for extra context like the "recipe" in "Save recipe", or a heading that sighted users don't need:
.visually-hidden:not(:focus):not(:active) {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
States¶
States are exposed too. From our test page:
- checkbox "Vegetarian" [checked]
- button "Bold" [pressed]
- button "Buy" [disabled]
- textbox "Name" [invalid]
- progressbar "Upload"
Native elements report state automatically — a checked checkbox is [checked] without
you doing anything. The toggle button and the invalid state came from ARIA attributes
(aria-pressed="true", aria-invalid="true"), which you have to keep in sync yourself
(lesson 04).
How It Actually Works¶
The browser derives the accessibility tree from the DOM and the computed styles:
- Start from the DOM. Remove nodes that aren't rendered (
display: none,visibility: hidden) or are hidden witharia-hidden="true"orinert. - Compute each node's role: an explicit
roleattribute if present and valid, otherwise the element's implicit role from the HTML-AAM mapping (which sometimes depends on context — a<header>isbanneronly when it isn't insidearticle/section/main...). - Prune nodes that don't matter for assistive tech: generic
divs andspans with no role, name or interesting content are "ignored" and their children promoted. Images withalt=""get apresentationrole and are dropped. - Compute names and descriptions with the accname algorithm.
- Compute states from native state (checked, disabled, required, the
openattribute on<details>...) and ARIA attributes. - Expose it through the platform API. Screen readers query the tree and listen for events (focus moved, value changed, children added) to know when to speak.
Because the tree depends on CSS, styling decisions are accessibility decisions. For
example, older browser versions dropped the semantics of elements given display:
contents, and list-style: none on a list causes Safari/VoiceOver to stop announcing it as a list
unless it's inside <nav> (a deliberate WebKit heuristic — add role="list" if the list
semantics matter).
Common mistakes¶
- Icon buttons and icon links with no name.
- Missing or file-name
alttext. aria-hidden="true"on focusable elements, or on a parent of them.- Hiding content with
opacity: 0or off-screen positioning when it should be gone for everyone. - Believing a clean automated scan means accessible. Automated tools catch only part of the problem (published estimates vary widely); names that exist but are wrong, confusing focus order and unusable custom widgets need a human.
- Treating accessibility as a final QA step rather than a property of the markup you write from the start.
Exercise¶
- Open the accessibility tree for your Level 2 product page. Find every interactive element and write down its role and name. Are any names missing or confusing?
- Build the ten-element naming example and inspect each one in devtools' Accessibility
pane. Change
aria-labelledbyto point at a non-existent id and see what happens. - Turn on a screen reader (VoiceOver: Cmd+F5 on macOS; NVDA is free on Windows; TalkBack on Android) and navigate a page by headings, then by links. Five minutes of this teaches more than any article.
- Add a visually-hidden heading to a section that lacks one visually, and confirm it appears in the heading list of your screen reader or devtools.