Detect accessibility regressions between pull requests without punishing existing debt.
Unlike standard axe CI integrations that pass/fail absolutely, WCAG_PR_Checker compares accessibility health over time. It blocks a PR only if it makes things worse so teams with legacy debt can mitigate new issues without first fixing everything.
View more information on the website.
- Base vs PR: The action compares the base branch (e.g.
main) to the PR branch β either by building both and serving static files, or by scanning live deployment URLs. - Runs axe-core on both via Playwright.
- Diffs the results β new violations are flagged, resolved violations are celebrated.
- Posts a structured comment to the PR and optionally fails the check.
Add this to .github/workflows/a11y.yml in any repository:
name: Accessibility Regression Check
on:
pull_request:
jobs:
a11y:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Run accessibility check
uses: zachkklein/WCAG_PR_Checker@v2
with:
APP_DIR: '.'
BUILD_DIR: 'public'
URLS: '/'
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}- Set
APP_DIRto the location of your projects root directory. - Set
URLSto contain the routes on your website you wish to check for accessibility issues. Separate routes with a comma (e.g., running the workflow on / and '/contact' would look like `URLS: '/,/contact').
- Ensure that you have granted write access by setting the
permissionssection above to:
permissions:
contents: write # required for git-auto-commit push
pull-requests: write- In the GitHub repository that you integrate this workflow into, set the
OPENROUTER_API_KEYby navigating toSettingsthenEnvironemnt VariablesthenActionsand add the key as aRepository Secret.
| Input | Description | Default |
|---|---|---|
OPENROUTER_API_KEY |
API key for OpenRouter. Required to enable the AI auto-fixer. Add as a repository secret. | `` |
APP_DIR |
Path to your app directory relative to repo root. Use "." if your app is at the root. |
. |
BUILD_DIR |
Static build output directory (dist, out, build). |
dist |
BUILD_COMMAND |
npm script to build your app. | build |
URLS |
Comma-separated URL paths to scan. | / |
IGNORE_RULES |
Comma-separated axe rule IDs to skip (e.g. "duplicate-id,color-contrast"). |
`` |
FAIL_ON_REGRESSION |
Fail the check when new violations are found. Set "false" to report only. |
true |
IMPACT_LEVEL |
Minimum severity to track: minor, moderate, serious, critical. |
moderate |
WAIT_FOR_NETWORK_IDLE |
Wait for network idle before scanning. Recommended for SPAs. | true |
EXTRA_WAIT_MS |
Additional milliseconds to wait after page load before scanning. | 500 |
TOKEN |
GitHub token with pull-requests: write. |
github.token |
BASE_URL |
Base branch deployment URL. If set with PR_URL, skips local build/serve and scans these URLs (see Preview URL mode). |
`` |
PR_URL |
PR preview deployment URL. If set with BASE_URL, skips local build/serve. |
`` |
| Output | Description |
|---|---|
new_violations |
Number of new violations introduced by this PR |
resolved_violations |
Number of violations resolved by this PR |
regression |
"true" or "false" |
When regressions are found, the action posts a comment like this:
Accessibility Check β Regressions Found
| | Baseline | This PR | Delta |
|------------------|----------|---------|--------|
| Total violations | 12 | 14 | +2 |
| π΄ Critical | 0 | 1 | +1 β¬οΈ |
| π Serious | 3 | 4 | +1 β¬οΈ |
New Violations (2)
| Impact | Rule | WCAG | Page | Selector | Docs |
|--------------|-----------------|-------------|-------|-------------|------|
| π΄ critical | color-contrast | WCAG 2.1 AA | / | #submit-btn | docs |
When neither BASE_URL nor PR_URL is set, the action checks out both branches, builds each, serves the static output with npx serve, and scans localhost. This only works when your app produces a static export (e.g. a folder of HTML/JS/CSS).
| Framework | Build command | Output dir | Notes |
|---|---|---|---|
| Vite | vite build |
dist |
Works out of the box |
| Next.js (static) | next build |
.next/server/app |
Requires output: 'export' in next.config.ts and no dynamic routes (or use generateStaticParams) |
| Create React App | react-scripts build |
build |
Works out of the box |
| Nuxt | nuxt generate |
.output/public |
Use static generation mode |
One way to verify your build folder is to run a build in your repository (i.e. with npm run build when using Next.js, and copy the file path to index.html).
When both BASE_URL and PR_URL are set, the action skips checkout, install, build, and serve. It only installs its own dependencies and Playwright, then scans the two URLs you provide. Use this for:
- Next.js (or any stack) with PR preview deployments (e.g. Vercel, Netlify).
- Any app where the base and PR are already deployed and you have two URLs to compare.
Vercel exposes the PR preview URL in the workflow; you can pass your production or main-preview URL as BASE_URL and the PR deployment as PR_URL.
- uses: zachkklein/WCAG_PR_Checker@main
with:
APP_DIR: '.'
BUILD_DIR: 'dist'
URLS: '/,/about'- uses: zachkklein/WCAG_PR_Checker@main
with:
APP_DIR: 'frontend'
BUILD_DIR: '.next/server/app'
URLS: '/,/dashboard'Use deployment URLs instead of building locally. No BUILD_DIR or build step β the action only scans the given URLs.
- uses: zachkklein/WCAG_PR_Checker@main
with:
BASE_URL: 'https://your-app.vercel.app' # production or main preview
PR_URL: ${{ steps.deploy.outputs.url }} # or env from Vercel GitHub Action
URLS: '/,/about,/projects'If you use the Vercel GitHub Action or similar, the PR preview URL is often available as an output or env var β pass that into PR_URL.
- uses: zachkklein/WCAG_PR_Checker@main
with:
FAIL_ON_REGRESSION: 'false'
URLS: '/,/about,/contact'- uses: zachkklein/WCAG_PR_Checker@main
with:
IMPACT_LEVEL: 'serious'
URLS: '/'a11y-diff-action/
βββ action.yml # Action definition and inputs
βββ package.json # Self-contained dependencies
βββ src/
βββ scan.js # Playwright + axe-core scanner
βββ diff.js # Violation diffing logic
βββ comment.js # PR comment formatting and posting
βββ auto-fix.js # Optional: AI fixes and commits back to PR (needs OPENROUTER_API_KEY, contents: write)
MIT License -- See LICENSE.txt