A standard and a clever platform answer are only claims until something checks them. Accessibility testing is where many teams get lost — they run an automated scanner, see green, and call it accessible. The honest picture is layered, and most of it isn't a scanner's job. (Writing these very tests turned up two real barriers on this site — proof the layers earn their place.)
A layered job, not a button
Each layer is cheaper and broader than the one above it, so it clears the easy ground and frees the slow, human layer for what only a person can judge. None replaces the next. Every layer below points at the real artifact in this repo — the suite you can open and run.
The layers also answer a fair question: tested for whom? In WebAIM's tenth screen-reader survey (1,539 respondents, early 2024), 91.3% reported using a screen reader on a mobile device — while most testing happens at a desktop. And the frustrations they ranked highest — CAPTCHA, interactive elements behaving unexpectedly, unclear links and buttons — are judgment calls no scanner flags. Only about a third felt the web had become more accessible. The layered model exists because the failures that matter most live where automation isn't looking.
01 · Static
Lint & types
Catches
Catches Mechanical mistakes before the app even runs — unlayered CSS, mixin order, a mistyped token, a type error.
Misses
Misses Knows nothing about the rendered page or a real user.
Stylelint · vue-tsc
npm run lint:css
npm run typecheck
02 · Unit
Pure logic
Catches
Catches A rule you wrote, proven in isolation — here, that the theme picker’s clamp can’t be dragged into a label that fails AA at any hue.
Misses
Misses Tests the maths you extracted, not whether it’s wired into the DOM.
Vitest
tests/unit/themePickerMath.test.ts
tests/unit/baseline.test.ts
03 · Automated DOM
axe sweep
Catches
Catches The machine-checkable slice: text contrast, missing names and labels, broken ARIA, landmark and heading structure.
Misses
Misses Roughly two-thirds of WCAG is invisible to it — a clean run is necessary, never sufficient.
Playwright · axe-core
tests/e2e/a11y.spec.ts
04 · Behavioural & human
Keyboard · screen reader · judgment
Catches
Catches What automation can’t reason about: focus order, whether a custom control actually operates, whether meaning survives without colour, whether a change is announced.
Misses
Misses Slower, and ultimately needs a person — which is why the layers below clear the easy ground first.
Playwright keyboard specs · a human with a screen reader
tests/e2e/keyboard.spec.ts
tests/e2e/inversion.spec.ts
manual passes
The slow human layer starts with a screen reader you already own. If you have never heard a page read aloud, a screen reader's first fifteen minutes (sheet A·03) has the keystrokes that matter and a plan for hearing your own page.
One more layer costs nothing and isn't in the diagram: reader mode. It rebuilds the page from semantics alone — headings, lists, article structure — so a page that survives the trip probably has its bones right, and a page reader mode can't parse is one a screen reader is likely fighting too. Thirty seconds, no tooling.
Reading the accessibility tree
A screen reader doesn't read your HTML, and it doesn't see your pixels. The browser distils every element into a parallel structure — the accessibility treeAccessibility treeThe browser’s distilled version of the page that assistive technology actually reads: for every element that matters, a role, a name, and its current state. DevTools can show it to you. — and that is what gets spoken: for each node a role (what is this?), a name (what is it called?), and, for controls, a value or state (what is it right now?). WCAG 4.1.2 is literally titled Name, Role, Value; it asks that this tree end up complete. What you hear in a screen reader's first fifteen minutes is this tree, read aloud.
You can read it directly. In Chrome or Edge, select an element in the Elements panel and open the Accessibility pane; in Firefox it's the Accessibility tab in the inspector; Safari shows the same under the Node sidebar in Web Inspector. Try it on the four examples below — none of the captions ask to be believed. Select the element and read what the browser actually computed. The pane is one of eight built-in tools — the DevTools sheet inventories the rest.
01 · Styled only
Save draft
<div class="btn">Save draft</div>
Role
generic
Name
none
From
—
To the tree this is text in a box: not focusable, not pressable, nothing to announce. And because nothing says it was meant to be a button, a scanner has nothing to flag — the quiet failure.
02 · The native element
<button>Save draft</button>
Role
button
Name
“Save draft”
From
its content
Same pixels, different tree. The element brings the role, the focus stop, and the keyboard behaviour; the text becomes the name for free.
for wires the two together: the field is named in the tree, and clicking the label focuses it — one attribute, two wins.
04 · ARIA on top
<button aria-label="Add to cart, size medium">Add to cart</button>
Role
button
Name
“Add to cart, size medium”
From
aria-label
aria-label replaces the content in the tree entirely. Keep the visible words at the front — voice-control users press buttons by saying what they see (WCAG 2.5.3).
When several naming mechanisms collide, the name computation settles it in strict order: aria-labelledby beats aria-label, which beats the native sources — the <label>, an image's alt, the element's own content — with title as a last resort. The practical rule hiding in that order: prefer the native source, because it stays visible, translated, and true; and when you must layer ARIA on top, start with the visible words. The tree is the contract between your markup and every assistive technology — the rest of this chapter is ways of checking the contract held.
What automation can and can't see
This is the part that's rarely spelled out. An automated pass like axe is excellent at a specific slice of WCAGWCAGThe Web Content Accessibility Guidelines — the W3C standard (2.2 is current) that most accessibility law worldwide points at. Chapter 01 walks its timeline. WCAG 3.0 is a working draft, years from done, and will not replace 2.2. and misses the rest entirely. Here's a sample of real defects against the methods that catch them — read the axe column top to bottom and watch it run out.
A sample of accessibility defects and which test method actually catches each. Caught, partial (flagged but not fully judged), or missed.
Defect
Automated (axe)
Keyboard (by hand)
Screen reader / judgment
Text contrast below 4.5:1WCAG 1.4.3
●Caught
○Missed
●Caught
Control has no accessible nameWCAG 4.1.2
●Caught
○Missed
●Caught
Form field not tied to a labelWCAG 1.3.1
●Caught
○Missed
●Caught
Skipped heading levelWCAG 1.3.1
◐Partial
○Missed
●Caught
Custom control not keyboard-operableWCAG 2.1.1
◐Partial
●Caught
●Caught
Focus order fights reading orderWCAG 2.4.3
○Missed
●Caught
●Caught
Focus indicator too faint to seeWCAG 2.4.11
○Missed
●Caught
●Caught
Icon / border contrast (non-text)WCAG 1.4.11
○Missed
○Missed
●Caught
Meaning carried by colour aloneWCAG 1.4.1
○Missed
○Missed
●Caught
Status change not announcedWCAG 4.1.3
○Missed
○Missed
●Caught
Loading state never announcedWCAG 4.1.3
○Missed
○Missed
●Caught
Read down the axe column: it clusters at the top, then falls away. Automated tools verify the machine-checkable part of WCAG — perhaps a third of it — and that part is genuinely worth automating so a human never has to re-check it. But focus order, operability, meaning that survives without colour, whether a change is announced — that's most of the standard, and it lives in the columns a machine can't fill in. A green axe run means “no known violations,” never “accessible.”
The listening room is that column as a whole page: eleven planted barriers, and a scanner that reports the page clean.
CSS that audits
The selector engine itself can be a testing layer. A handful of modern selectors — :not(), :has(), attribute checks — describe accessibility smells precisely enough to paint them on screen: no build step, no extension, just a stylesheet you drop into DevTools on any page. Flip the toggle and watch four planted defects light up while their healthy twins stay quiet.
Broken on purpose — four planted defects, two healthy controls. Behind glass (inert and hidden from assistive tech): even broken examples must never harm a real visitor.
Four selectors, four catches: the alt-less image, the empty icon button, the label-less search field, and the new-tab link with no warning — while their healthy twins stay untouched. axe would flag the first three; the new-tab hint is invisible to it. A debug stylesheet isn't a test suite, but it runs everywhere CSS runs: in the browser, on any page, with zero tooling.
Filing what you find
Most audit findings die in a backlog, and it's usually the report's fault. A ticket titled "fails WCAG 1.3.1" competes against feature work and loses — and copying severity from the conformance level grades how foundational the rule is, not how much this instance hurts. Grade each finding by user impact instead, with three questions: is the task blocked, or just harder? how often does someone hit it — critical path or corner? and is there a workaround a non-expert would actually find? A silent div-button on checkout and a redundant alt text can fail at the same level; they are not the same bug.
The report itself has one job: let a teammate who wasn't in the audit act on it. So the headline names the barrier — what a person cannot do — not the criterion. The setup names the assistive tech and browser together, because bugs live in pairs. The criterion appears once, as a reference, so nobody has to become a WCAG lawyer to close the ticket. And it's one barrier per ticket, sized like any other bug — never one mega-ticket called "accessibility audit". Filled in the way an audit of a fictional checkout might:
md
## Barrier: payment method can't be chosen with a keyboard
**Impact**: Blocker — checkout cannot be completed
**Where**: /checkout, the payment method cards
**Setup**: VoiceOver + Safari 26 (also reproduced: NVDA + Firefox)
**Steps**
1. Tab through the checkout form
2. Focus jumps from the address field straight to "Pay now"
3. The payment cards are never reachable
**Heard**: "Pay now, button" — no mention a choice was expected
**Expected**: each card focusable, announcing name, role, checked state
**Reference**: WCAG 2.1.1 Keyboard
**Suspected fix**: the cards are divs with click handlers —
a native radio group does all of this for free
The last line is where this site's whole argument cashes out: most accessibility bugs are a native element that got rebuilt by hand, so most suspected fixes are swaps — the same swaps the agent skill teaches. And when a finding has no obvious fix line, that's fine. Naming the barrier precisely is the report's real work.
Performance is accessibility
Performance work usually files under "nice to have." For assistive tech it's load-bearing. A screen readerScreen readerAssistive technology that speaks the page aloud (VoiceOver, NVDA, JAWS), navigating by the semantics in the markup — the reason markup quality is something you can hear. walks the accessibility tree through the same main thread your JavaScript blocks — every long task is a stretch of silence between a keypress and hearing where you landed. Motion that stutters is harder on vestibular disorders than motion that glides, which is why this site animates only transform and opacity: honoring prefers-reduced-motion is step one, keeping the remaining motion off the main thread is step two. And input latency is felt hardest by the people who type through switch devices or sticky keys — for them, a sluggish keystroke isn't an annoyance but the interface going unresponsive mid-word. Less JavaScript isn't just this site's aesthetic; it's why the assistive-tech experience stays quick.
Where this argument stops
Everything on this site stays on one side of a line: what HTML and CSS guarantee before any JavaScript arrives. The other side is real, and application work lands on it daily — ARIA widget patterns, live regions that announce what just changed, focus management after a route change or a deletion. Those are JavaScript's territory, harder to get right than anything shown here, and claiming CSS covers them would be exactly the kind of overclaim this site argues against.
Crossing the line is not the mistake; skipping the first question is. Ask whether the platform already knows the state: a field that was touched and is invalid, a dialog that is open, an input that is still empty. Where an element or a selector already carries it, a script that publishes the same state again is a second source of truth, and the two drift. Where the platform has no element for the job, such as counting the characters that remain, the script is the right tool, and what matters is where it writes: text in an output element, an attribute such as aria-pressed, a status message, focus. A custom property is not in the accessibility tree: it can change how something looks, but it is not a name, a state or a message.
When you cross the line, the ARIA Authoring Practices Guide is the map — and the habit from this chapter still applies: test what you build, in layers, with the people and tools the layers stand in for.