Pa11y exits 2 for page errors and 1 for its own faults, and that distinction is the whole CI contract
Pa11y is your automated accessibility testing pal
At a glance
- What is it?
- Pa11y is a command line and Node.js accessibility tester that runs HTML CodeSniffer or axe against a page and reports through five built-in reporters. Its most useful behaviour for automation is a three value exit code plus a configurable severity threshold, and its most confusing one is that the reporter list in the help output and the reporter list in the documentation do not match.
- Who is it for?
- Pa11y fits a team that wants a headless accessibility check in a build pipeline with a severity threshold it controls, and that can pick a runner per run. It does not fit a team whose browser tests must run on an older Node, since version 10 requires an even numbered Node 22.13 or newer and older runtimes need version 9 or below.
- Can I use it commercially?
- Yes, with conditions. LGPL-3.0 is a weak copyleft licence: you can use it inside commercial and closed-source software, but if you distribute changes to its own files, you must publish those changes under the same licence.
- Is it still maintained?
- Yes. The repository last received commits 5 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 13, 2026, and from our analysis. They are not legal advice.
Star history
- pa11y/pa11y4,599 ★ · +62
| Repository | 2026-09-23 | 2026-11-13 |
|---|---|---|
| pa11y/pa11y | 4.5k | 4.6k |
62 Stars in the last 51 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
Two runners, and combining them is a repeated flag
The whole tool in one line is:
pa11y https://example.comThe default test runner is HTML CodeSniffer, and axe is also supported. Choosing the other one is a flag:
pa11y https://example.com --runner axeAnd because the flag repeats rather than taking a list, running both in a single pass means passing it twice:
pa11y https://example.com --runner axe --runner htmlcsThe dependency list explains the split: `@pa11y/html_codesniffer` at ^2.6.0 and `axe-core` at ~4.13.0 are both direct dependencies, and Puppeteer at ^25.9.0 is what loads the page for either of them.
Three exit codes, and the third one is a bug in your page
The exit code table is the contract a build pipeline depends on:
- `0`: Pa11y ran successfully, and there are no errors - `1`: Pa11y failed run due to a technical fault - `2`: Pa11y ran successfully but there are errors in the page
The distinction between 1 and 2 is the useful part. A code of 1 means the tool did not run, which is a problem with your environment or your URL, and a code of 2 means the tool ran and found something. Treating both as failure loses the distinction between a broken pipeline and a broken page.
By default only accessibility issues with a type of `error` exit with a code of 2. Warnings and notices do not. That is configurable with the `--level` flag, and the documented values are:
- `error`: exit with 2 on errors only, exit with 0 on warnings and notices - `warning`: exit with 2 on errors and warnings, exit with 0 on notices - `notice`: exit with 2 on errors, warnings and notices - `none`: always exit with 0
So the default is the strictest useful setting in terms of noise, and the loosest in terms of signal. Which one you want depends on whether your pipeline can tolerate warnings blocking a release.
A threshold of ten passes a page with nine errors
There is a flag that changes the exit code based on a count, and it is the one to think hardest about before adopting.
The stated use case is CI, where you do not want to break the build. The example given returns exit code 0 on a page with 9 errors and exit code 2 on a page with 10 or more:
pa11y https://example.com --threshold 10So a threshold is a ratchet. Set it once, and the build enforces a floor rather than a standard.
That is a reasonable way to introduce accessibility checking into a codebase that already has problems, because it lets the count move in one direction without anyone having to fix everything first. It is a poor long-term policy if nobody lowers it, because a page that regresses from nine errors to ten fails while a page sitting at nine passes indefinitely.
Note that this is a count, not a severity. Nine serious errors and nine trivial ones are the same number.
The cleaner alternative in the same area is the `--ignore` flag, which suppresses named issue codes.
Ignoring is by issue code, and there are two ways to spell the list
The ignore flag takes issue codes and accepts two forms.
Separated by semi-colons:
pa11y https://example.com --ignore "issue-code-1;issue-code-2"Or by repeating the flag:
pa11y https://example.com --ignore issue-code-1 --ignore issue-code-2Both work, and the second composes better with generated commands.
The flag works on issue codes, so an ignore list is a list of rules you have decided not to enforce.
So an ignore list is a statement about a rule, and adding to one is a decision about policy rather than about a page. If you keep one in a config file rather than on the command line, the list becomes reviewable in version control, which is the only way it stays honest over time.
Configuration can also come from a JSON file, by default pa11y.json in the current directory, or one named with --config.
The help output and the documentation list different reporters
This is a small inconsistency, and it will cost you a few minutes if you hit it.
The help text lists three reporters:
-r, --reporter <reporter> the reporter to use: cli (default), csv, jsonThe reporters section lists five: `cli` for human-readable output, `csv`, `html` for an HTML page, `json` for a JSON array, and `tsv` for tab-separated values.
So `html` and `tsv` are documented but not shown in the help output. The README lists both as built-in reporters, but the help text is the thing you read when you cannot remember the flag.
The default is `cli` in both places, which is the other thing worth knowing: running `pa11y https://example.com` with no reporter gives you a human-readable list rather than machine-readable output.
For machine consumption you therefore have to be explicit. Writing results to a file is done by redirecting, with the reporter selected separately:
pa11y https://example.com > report.csv --reporter csvSo if you are scripting against this, choose the reporter every time rather than relying on the default.
A custom reporter has to declare which versions it supports
Writing your own reporter is a documented extension point with one unusual requirement.
Reporters must be CommonJS modules, and may be installed packages or local files. Pa11y checks for reporter modules in a specific order and loads the first one it finds. With `--reporter rainbows`, the locations checked in order are an installed package named `pa11y-reporter-rainbows`, then a module file located at `<cwd>/rainbows`. If no reporter is found, Pa11y errors.
Each candidate is loaded with the standard Node.js `require`, so normal CommonJS module resolution applies and the module has to be resolvable that way.
The unusual requirement is the `supports` property. A reporter must export a string property named `supports`, which is a semver range indicating which versions of Pa11y the reporter supports:
exports.supports = '^8.0.0';So a custom reporter carries a compatibility declaration.
The rest of the contract is five methods, each returning one string, with a promise allowed if the work is asynchronous: `begin()` when pa11y starts, `error(message)` for a technical error, `debug(message)`, `info(message)`, and `results(results)` for a test run's results.
Version 10 needs an even numbered Node 22, and that is a hard floor
The requirements section states it in one sentence: Pa11y 10 requires an even numbered version of Node.js 22.13.0 or above, and an older version of Node.js can be used with Pa11y 9 or below.
The manifest agrees, with the engines field set to `^22.13.0 || >=24`.
So the Node constraint is narrower than a minimum. It is a range that excludes odd major versions entirely, which means Node 23 is not supported and Node 21 is not supported, and the two supported lines are 22 from 22.13 and anything from 24 up.
If your build runs on an older Node, the answer is not to install Pa11y 10 anyway. The documented answer is Pa11y 9 or below, and the repository carries a MIGRATION.md at the root alongside a TROUBLESHOOTING.md.
The release history shows how recently the floor moved. Version 10.0.0 published on 2026-08-28, after 9.1.1 in February 2026 and 9.1.0 earlier that month.
For local setup the README suggests nvm, and the Windows note says Pa11y is usable via the bundled Node.js application as well as the Windows command prompt.
Editorial conclusion
Pa11y fits a team that wants a headless accessibility check in a build pipeline with a severity threshold it controls, and that can pick a runner per run. It does not fit a team whose browser tests must run on an older Node, since version 10 requires an even numbered Node 22.13 or newer and older runtimes need version 9 or below. Before wiring it into CI, decide whether a threshold is acceptable as a policy, because --threshold 10 will pass a page with nine errors, and check the reporter you need against the README's reporters list rather than the help output, since the two disagree.
Frequently asked questions
what is pa11y
An automated accessibility testing tool that runs accessibility tests on your pages via the command line or Node.js, so you can automate your testing process. It uses HTML CodeSniffer by default with axe also supported, and it is LGPL-3.0 licensed with version 10.0.0 published on 2026-08-28.
how to install pa11y
Install it globally with npm install -g pa11y for the command line, or add it to a project as a development dependency with npm install pa11y --save-dev and require it in JavaScript. Pa11y 10 requires an even numbered version of Node.js 22.13.0 or above; an older Node.js can be used with Pa11y 9 or below.
how to use pa11y
Run pa11y https://example.com to test a page, pa11y https://example.com --runner axe to use axe instead of the default HTML CodeSniffer, or pass --runner twice to use both in one run. Pick the output format with --reporter, ignore known rules with --ignore, and point at a config file with --config, where command-line options take priority over file values.
is pa11y free
It is open source under the LGPL-3.0-only license, and the repository is public with a CONTRIBUTING.md file at the root.
pa11y vs axe core
The README does not position them as competitors. It names axe as one of two test runners, alongside HTML CodeSniffer which is the default, and both are direct dependencies of the package. You select between them with the --runner flag, and can pass it twice to run both in the same pass.
What exit codes does Pa11y use?
0 when Pa11y ran successfully and there are no errors, 1 when Pa11y failed to run due to a technical fault, and 2 when it ran successfully but there are errors in the page. The --level flag decides which severities produce 2: error, warning, notice or none.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/pa11y-pa11y)Star history chart for your README
[](https://hysenlabs.com/projects/pa11y-pa11y)Was this useful?