Guide
Accessibility testing in CI
Accessibility bugs are cheapest to fix in the pull request that creates them. Running a checker in continuous integration turns accessibility from an occasional audit into a normal test: a new button without a name fails the build just like a broken import, and the report points to the exact line and the fix.
Why run accessibility checks in CI
A one time audit finds problems, but it does not stop new ones. Teams that fix two hundred issues in a sprint often find a hundred new ones six months later, because nothing changed in how code gets merged. A check in CI changes that. Each pull request is compared against the same rules, the author sees the problem while the code is still fresh, and reviewers no longer need to spot missing labels by eye.
Automated checks will not catch everything (reading order, caption quality and the usefulness of alt text still need people), but they reliably catch the mistakes that are both common and serious: missing names, missing labels, low contrast, broken ARIA, removed focus outlines and disabled zoom.
The CLI as a quality gate
The CLI exits with code 1 when it finds a problem at or above the severity you choose with --fail-on. The default is minor, which fails on anything. Use none to report without failing.
# Fail only on critical and serious findings
npx a11ymedic "dist/**/*.html" --fail-on serious
# Report everything, never fail (useful for a first run)
npx a11ymedic "dist/**/*.html" --fail-on none --format html -o a11y-report.html
# Machine readable output for dashboards
npx a11ymedic "dist/**/*.html" --format json -o a11y.jsonPoint it at your built HTML (the output of a static site generator, or prerendered pages) for a fast static check. For pages that need JavaScript to render, see the section on live URLs below.
GitHub Actions
On GitHub Actions you do not need any extra configuration. When the GITHUB_ACTIONS environment variable is set, a11ymedic prints annotations, so each finding shows up inline on the changed file in the pull request, and writes a job summary with the counts and the fixes. You can force it on or off with --github and --no-github, or produce the same output anywhere with --format github.
name: Accessibility
on:
pull_request:
push:
branches: [main]
jobs:
a11y:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run build
- run: npx a11ymedic "dist/**/*.html" --fail-on seriousIf you also want a downloadable report, add --format html -o a11y-report.html to a second run and upload the file as a build artifact. The HTML report is self contained, so anyone on the team can open it without installing anything.
Unit and component tests with Vitest or Jest
CI checks on built pages catch problems late in the pipeline. Component tests catch them earlier, where the markup is written. a11ymedic ships a toHaveNoViolations matcher. Import the setup file once and use it like any other assertion. HTML without <html> or <body> is treated as a component fragment, so page level rules such as the document title and the main landmark are skipped automatically.
import { test, expect } from "vitest";
import { audit } from "a11ymedic";
import "a11ymedic/vitest";
test("icon button has a name", async () => {
const html = `<button type="button" aria-label="Close"><svg aria-hidden="true"></svg></button>`;
expect(await audit(html)).toHaveNoViolations();
});
test("settings page has no serious problems", async () => {
const html = renderSettingsPage();
expect(await audit(html)).toHaveNoViolations({ severity: "serious" });
});For Jest, register the matcher in your config instead of importing it in each file:
module.exports = {
testEnvironment: "node",
setupFilesAfterEnv: ["a11ymedic/jest"],
};When a test fails, the message lists each finding with its rule, the element, and the before and after code, so the fix is in the test output. The matcher also accepts an ignore list of rule ids for cases you have reviewed and accepted.
Testing live URLs and rendered apps
Single page apps render most of their markup in the browser, so the raw HTML file is almost empty. Pass a URL instead. If Playwright or Puppeteer is installed in the project, the CLI loads the page in a headless browser and audits the rendered result with real computed styles and element sizes. That is the most accurate mode for color contrast and for the 24 by 24 pixel target size rule. Without a browser it fetches the HTML and runs a static check. Use --browser to require the headless browser, or --no-browser to forbid it.
npm install --save-dev playwright
npx playwright install chromium
npx a11ymedic https://preview.example.com/ https://preview.example.com/pricing --browser --fail-on seriousRolling it out on an existing codebase
If the first run reports 300 findings, a strict gate will block every pull request and the team will turn it off. Roll it out in steps instead:
- Report only. Run with
--fail-on noneand publish the report. Fix the shared components first; one header fix can remove dozens of findings. - Apply the safe fixes.
npx a11ymedic fix "src/**/*.html" --writeapplies changes that need no human decision, such asalt=""on decorative images or a missinglangwhen the language can be detected. Review the diff and commit it. - Gate on critical. Switch to
--fail-on critical. New critical problems now fail the build. - Tighten. Move to
--fail-on serious, then lower as the backlog shrinks.
Use --exclude with a CSS selector to skip third party widgets you cannot change, and --disable to turn off a rule while a known issue is being worked on. Keep those exceptions in one place, such as an a11y script in package.json that every developer and the CI job run the same way, so they stay visible and get reviewed like any other code.
Pair the gate with a short manual pass on every release: keyboard only, zoom to 200 percent, and a screen reader check on the main flows. The WCAG 2.2 checklist marks which items need that human review, and the install section covers every setup.
Frequently asked questions
How do I add accessibility testing to GitHub Actions?
Build your site, then run npx a11ymedic on the generated HTML with --fail-on serious. On GitHub Actions it adds inline annotations to the pull request and writes a job summary automatically.
Can I use toHaveNoViolations with Vitest?
Yes. Import a11ymedic/vitest once, then call expect(await audit(html)).toHaveNoViolations(). You can pass a minimum severity or a list of rule ids to ignore.
Should accessibility tests fail the build?
Yes, once the existing backlog is under control. Start in report only mode, fix shared components, then fail on critical findings and tighten the threshold over time.
Do I need a headless browser for accessibility testing in CI?
Not for static HTML, which is checked quickly without one. For pages rendered by JavaScript, and for the most accurate contrast and target size results, install Playwright or Puppeteer and pass URLs.