Audit, test, and maintain
Run automated checks
Use validators, linters, browser audits, and test libraries to catch deterministic problems early.
Automated checks catch the mistakes a machine can prove: an input with no label, an image with no alt, an aria-controls pointing at an id that doesn’t exist, a duplicate id, text that fails the contrast ratio. They catch them fast, on every commit, without anyone remembering to look. That’s their whole value.
They can’t tell you whether the alt text makes sense, whether focus order matches the task, or whether “Invalid form” helps anyone. Estimates vary, but automation finds somewhere around a third of real barriers. Use it for that third and be honest about the rest.
Run axe from the terminal
The most common engine is axe-core. It runs in the browser extension, inside Lighthouse, and from the command line. Start the flawed page locally and point the CLI at it:
npx @axe-core/cli http://localhost:3000/register
The output lists each rule that failed, with the selectors of the elements:
Violation of "label" with 2 occurrences!
Form elements must have labels. Correct invalid elements at:
- input[placeholder="Full name"]
- input[placeholder="Value"]
Violation of "color-contrast" with 1 occurrences!
Correct invalid elements at:
- .hint
Violation of "region" with 3 occurrences!
Correct invalid elements at:
- .speakers
- .schedule
- .footer-links
3 Accessibility issues detected.
Every one of these matches a repair from earlier lessons: the placeholder-only fields, the #999 hint text, the content sitting outside landmarks. Notice what’s missing: the div Register button, the invented date picker, the focus that vanishes when a guest row is removed. No rule fires for behavior.
Put it in the test suite
A one-time scan drifts. A test runs on every pull request. With Playwright installed, add @axe-core/playwright:
import { test, expect } from '@playwright/test'
import AxeBuilder from '@axe-core/playwright'
test('registration page has no axe violations', async ({ page }) => {
await page.goto('http://localhost:3000/register')
const results = await new AxeBuilder({ page }).analyze()
expect(results.violations).toEqual([])
})
When someone reintroduces a placeholder-only field, the test fails with the same rule id and selector as the CLI. The label regression from the next lesson is guarded by exactly this.
Two tools, one list
Lighthouse uses axe-core internally, so running both mostly duplicates findings. Pair axe with something different: the W3C HTML checker at validator.w3.org/nu catches invalid nesting, duplicate ids and stray attributes that axe doesn’t. Merge the two outputs into one list, and remove the duplicates by selector.
Then review each remaining item in context. Contrast checks on text over a photo or gradient come back as “needs review”, not as a failure. Look at it yourself, decide, and record the decision. And for every item on your task list from lesson 1 that no tool covered, write “manual” next to it. That column is your test plan for the next lesson.
The two ways to get this wrong are mirror images. One team treats the green run as proof the page is accessible. Another team ignores the tool because it misses things. Both waste the third it does catch.
Try this on your own project: run axe and the HTML checker on one page, deduplicate, verify one flagged item is a false positive by hand, and list the tasks that remain uncovered.
Lesson completed