Overview
pa11y runs WCAG accessibility checks against a URL or a set of URLs. The default configuration fails on any error, which is too strict for codebases migrating incrementally and too loose for teams that want to catch regressions. A well-configured threshold strategy allows zero new violations while letting known legacy issues stay visible. The broader accessibility playbook lives in accessibility; running pa11y locally is covered in run-pa11y-locally.
Prerequisites
- pa11y installed:
npm install -D pa11y pa11y-ci. - A running local or staging server, or a set of public URLs to test.
- An existing pa11y run with baseline results so you know what the current violation count is.
npx pa11y http://localhost:8080 --reporter json > baseline.jsonSteps
1. Create a .pa11yci configuration file
pa11y-ci reads .pa11yci (JSON) at the project root. Start with sensible defaults.
{
"defaults": {
"timeout": 30000,
"wait": 500,
"standard": "WCAG2AA",
"runners": ["axe", "htmlcs"],
"ignore": [],
"threshold": 0
},
"urls": [
"http://localhost:8080",
"http://localhost:8080/blog",
"http://localhost:8080/contact"
]
}threshold: 0 means zero errors are permitted. This is the target state. On a codebase with existing violations, you will raise this number temporarily (step 3) and lower it as issues are fixed.
2. Add per-rule overrides with ignore patterns
Use ignore to exclude specific WCAG rules that are accepted as known exceptions. Document the reason in a comment file, since JSON does not support inline comments.
{
"defaults": {
"standard": "WCAG2AA",
"ignore": [
"WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail",
"color-contrast"
]
}
}Prefer ignoring specific rule codes over entire principles. Ignoring Principle1 silences too many rules and hides real regressions.
To ignore a violation only on a specific page, put the ignore key inside the URL entry rather than defaults:
{
"defaults": { "standard": "WCAG2AA" },
"urls": [
{
"url": "http://localhost:8080/legacy-form",
"ignore": ["WCAG2AA.Principle2.Guideline2_4.2_4_1.H64.1"]
}
]
}This scopes the exception to one page and prevents it from silently spreading. See html-aria-patterns for the correct ARIA patterns that fix these rules permanently.
3. Set a realistic threshold during migration
If the baseline has 40 existing violations, set threshold: 40 today and schedule a sprint to reduce it to zero. The threshold acts as a ratchet: never raise it, only lower it.
{
"defaults": {
"threshold": 40
}
}Track the number in a docs/accessibility-debt.md file or a GitHub issue with a checkbox list of rules to fix. See html-semantic-elements for the most common fixes.
4. Wire pa11y-ci to GitHub Actions
Run pa11y-ci against the deployed preview URL on every pull request. Use pa11y-ci (not pa11y) to test multiple pages in one run.
# .github/workflows/a11y.yml
name: Accessibility
on: [push, pull_request]
jobs:
pa11y:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: "22" }
- run: npm ci
- run: npm run build && npx serve -s public -p 8080 &
- run: sleep 3 && npx pa11y-ciFor server-rendered sites, start the dev server instead of serving the static build. See github-actions for caching and reuse patterns.
5. Tighten the threshold over time
Each sprint, reduce threshold by the number of violations fixed. When threshold reaches zero and ignore is empty, the accessibility baseline is clean. Remove the threshold key entirely; pa11y-ci defaults to zero.
Use the JSON reporter to track progress over time:
npx pa11y-ci --reporter json 2>/dev/null | jq '.totals'
# { "errors": 12, "warnings": 4, "notices": 7 }Verify it worked
# Zero errors on clean pages
npx pa11y-ci
# Expected: "Errors: 0"
# Confirm ignore patterns suppress only the intended rules
npx pa11y http://localhost:8080 --reporter json | jq '.[].code' | sort -u
# The ignored codes should not appear.
# Confirm CI fails when a new violation is introduced
# Temporarily add a <img> without alt, run pa11y-ci, expect non-zero exit.Common errors
- pa11y times out on JavaScript-heavy pages. Increase
timeoutto 60000 and addwait: 1000to let the page fully render before scanning. - Violations appear in CI but not locally. The CI build uses a different base URL. Check that
urlsin.pa11ycimatch the server address in the workflow. thresholdis set but CI still passes with new violations.thresholdis a maximum count, not a delta. If you add two violations and remove two, the count stays the same and CI passes. To catch regressions, track per-rule counts in a separate script.ignorearray has no effect. The rule code must match exactly, including the full dotted path. Copy the code frompa11y --reporter jsonoutput.axeandhtmlcsreport contradictory results. Both runners implement WCAG differently. Preferaxefor programmatic correctness; usehtmlcsfor structural checks. Remove one runner if the noise is high.