Pre-push a11y audit
Automatic accessibility verification that runs on every push when UI components change.
Accessibility is a convention in this codebase, not a nice-to-have. To make sure the convention is actually enforced, the pre-push git hook runs an accessibility audit against any UI component files you've changed on this branch — automatically, every time you git push.
You don't have to remember to run it. You don't have to opt in. If you touch packages/ui/src/components/, the audit runs.
The audit script lives in .husky/pre-push lines 36–80. If this page and the hook disagree, the hook wins.
Why it exists
The ITU platform serves content in six languages to a global audience that includes people using screen readers, keyboard navigation, and other assistive technology. Accessibility regressions are invisible until they aren't, and by the time a user reports one it has already shipped.
The a11y audit is the enforcement arm of the accessibility-first convention:
- Every time a UI component changes, its accessibility is re-verified.
- The verification happens on your machine, before the code leaves your branch.
- Results are captured as JSON and committed alongside the component so the current state is always traceable.
- If the audit finds a regression, you see it immediately — not days later in CI, not weeks later in production.
Put differently: the audit makes accessibility a property of the component file itself, not a task on someone's checklist.
How it works
The hook runs in three phases.
Detect changed components
Use git diff --name-only main...HEAD against packages/ui/src/components/ to list only the files that changed on this branch.
git diff --name-only main...HEAD -- packages/ui/src/components/
The result is normalised into a set of component slugs (lowercase, kebab-case). If no UI component files changed, the hook exits cleanly — no audit, no delay.
Extract variants and run the audit
Before auditing, the hook makes sure the variant manifest is fresh. The variants file is what tells the audit runner which visual states of each component to exercise.
pnpm --filter @itu/ui exec tsx scripts/extract-variants.ts
Then it runs the audit itself, passing the list of changed slugs:
pnpm --filter @itu/ui run a11y $SLUGS
Under the hood, pnpm a11y is tsx scripts/audit-a11y.ts — a Playwright + @axe-core/playwright script that renders each requested component in a headless browser and runs the axe-core accessibility ruleset against it.
Stage results back into the commit
For every component the audit touched, the hook stages the regenerated result file:
git add packages/ui/generated/a11y/<slug>.json
These JSON files are committed as part of your push. They are the artefact of record — the canonical statement of "here's what the a11y audit saw the last time this component changed."
Artifacts produced
| File | Written by | Purpose |
|---|---|---|
packages/ui/generated/variants/*.json | extract-variants.ts | Manifest of visual variants per component — input to the audit |
packages/ui/generated/a11y/<slug>.json | audit-a11y.ts | Axe-core findings for each audited variant of the component |
Both directories live under packages/ui/generated/. The a11y/ results are exported from @itu/ui via the ./a11y/* export in its package.json, which means other packages (notably the docs site) can import and render them.
Scope: only what changed
The hook deliberately does not re-audit every component on every push. It diffs against main and audits only the slugs that appear in the diff:
CHANGED_COMPONENTS=$(git diff --name-only main...HEAD -- packages/ui/src/components/ 2>/dev/null | \ sed -n 's|packages/ui/src/components/\([^/]*\).*|\1|p' | \ sort -u | \ tr '[:upper:]' '[:lower:]' | \ sed 's/\([a-z]\)\([A-Z]\)/\1-\2/g')
This keeps the feedback loop tight. A typical push touches one or two components, and auditing those two components takes seconds — not the tens of minutes it would take to audit all of them.
The trade-off is that a component you didn't touch can still have stale audit results on disk. That's fine: a full-codebase audit is a separate concern (run pnpm --filter @itu/ui a11y --all locally or in CI if you need it).
What if I see this hang?
The audit spins up Playwright and renders components in a real browser. That's usually fast, but if git push appears to hang after printing Running a11y audit for changed components: ..., here are the most common causes.
| Symptom | Likely cause | What to try |
|---|---|---|
| Hangs for a long time on one component | Audit is rendering many variants of a complex component | Wait it out; the first run per component is the slowest |
Fails with browserType.launch: Executable doesn't exist | Playwright's bundled Chromium isn't installed | pnpm --filter @itu/ui exec playwright install chromium |
Fails with Cannot find module 'tsx' | Dev dependencies aren't installed | pnpm install at the repo root |
Audit runs but no a11y/*.json files are staged | The audit crashed before writing results | Run the audit manually to see the error: pnpm --filter @itu/ui run a11y <slug> |
| Hangs immediately with no output | extract-variants.ts is stuck (e.g. infinite loop in a component file) | Check the component you most recently edited for runtime errors |
If you want to bypass the hook temporarily — for example, to push a work-in-progress branch to get a colleague's eyes on it — you can use git push --no-verify. Use this sparingly. It means the audit doesn't run, which means the convention isn't being enforced on that push.
Skipping the hook with --no-verify is acceptable for draft branches, but do not merge a branch to main without passing the audit. CI should reject any PR whose generated/a11y/*.json is out of sync with its component source.
Running the audit manually
You rarely need to invoke the audit yourself, but when you do:
# Audit a specific component pnpm --filter @itu/ui run a11y button
# Audit several components at once pnpm --filter @itu/ui run a11y button hero links-grid
# Audit every component (slow — full sweep) pnpm --filter @itu/ui run a11y --all
# Regenerate the variants manifest before an audit pnpm --filter @itu/ui exec tsx scripts/extract-variants.ts
Related
- The git hooks overview explains how the pre-push hook sits alongside the other hooks (commit-msg, pre-commit) in the enforcement chain.
- Component-level accessibility state is rendered on each component's documentation page under the "Accessibility" section, sourced directly from
packages/ui/generated/a11y/<slug>.json.