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.

Source of truth

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.

1

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.

bash
git diff --name-only main...HEAD -- packages/ui/src/components/
1 line of bash code

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.

2

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.

bash
pnpm --filter @itu/ui exec tsx scripts/extract-variants.ts
1 line of bash code

Then it runs the audit itself, passing the list of changed slugs:

bash
pnpm --filter @itu/ui run a11y $SLUGS
1 line of bash code

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.

3

Stage results back into the commit

For every component the audit touched, the hook stages the regenerated result file:

bash
git add packages/ui/generated/a11y/<slug>.json
1 line of bash code

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

FileWritten byPurpose
packages/ui/generated/variants/*.jsonextract-variants.tsManifest of visual variants per component — input to the audit
packages/ui/generated/a11y/<slug>.jsonaudit-a11y.tsAxe-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:

bash
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')
5 lines of bash code

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.

SymptomLikely causeWhat to try
Hangs for a long time on one componentAudit is rendering many variants of a complex componentWait it out; the first run per component is the slowest
Fails with browserType.launch: Executable doesn't existPlaywright's bundled Chromium isn't installedpnpm --filter @itu/ui exec playwright install chromium
Fails with Cannot find module 'tsx'Dev dependencies aren't installedpnpm install at the repo root
Audit runs but no a11y/*.json files are stagedThe audit crashed before writing resultsRun the audit manually to see the error: pnpm --filter @itu/ui run a11y <slug>
Hangs immediately with no outputextract-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:

bash
# Audit a specific component
pnpm --filter @itu/ui run a11y button
2 lines of bash code
bash
# Audit several components at once
pnpm --filter @itu/ui run a11y button hero links-grid
2 lines of bash code
bash
# Audit every component (slow — full sweep)
pnpm --filter @itu/ui run a11y --all
2 lines of bash code
bash
# Regenerate the variants manifest before an audit
pnpm --filter @itu/ui exec tsx scripts/extract-variants.ts
2 lines of bash code

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.