@surea11y/cli
v1.0.0
Published
Command-line WCAG 2.2 accessibility scanner for static HTML, with baseline gating and HTML/SARIF reports. Powered by @surea11y/core.
Maintainers
Readme
@surea11y/cli
Command-line WCAG 2.2 accessibility scanner for static HTML — baseline gating, HTML and SARIF reports, custom rules. Powered by @surea11y/core, the deterministic engine that tells you what it can't tell you.
npx @surea11y/cli scan ./index.html
npx @surea11y/cli scan https://example.com/ --tags wcag2a,wcag2aaInstall
npm install -g @surea11y/cli # globally
npm install --save-dev @surea11y/cli # or per-project, for CIRequires Node.js ^20.19.0 || ^22.13.0 || >=24.0.0.
What it does
$ surea11y scan ./index.html
surea11y scan: file:///path/to/index.html
pass: 6 fail: 3 cantTell: 4 notApplicable: 112
occurrences by tier: fail: 3 cantTell: 5
FAIL (3 rule(s)):
contrast-minimum (serious, 1 fail occurrence(s))
- html > body > main > p
Element has insufficient color contrast of 2.85:1 (foreground: #999999,
background: #ffffff, font size: 0px, font weight: normal). Expected
contrast ratio of 4.5:1 (normal text).
img-alt-present (serious, 1 fail occurrence(s))
- html > body > main > img
Missing alt attribute on <img>.
hint: Add an alt attribute (use alt="" only for decorative images).
cantTell — needs human review (4 rule(s)): contrast-computable, link-name-quality,
manual-review, page-title-patternsThat last line is the point of the engine: rules that cannot be settled from static markup are reported as cantTell and routed to a human, instead of being silently dropped or guessed at. cantTell never affects the exit code.
Exit code 0 = clean, 1 = at least one fail (the CI-gating case), 2 = usage error or the scan couldn't run.
What it can and can't scan
The CLI reads static HTML only — a local file, or the raw body of an HTTP(S) GET — and never executes page JavaScript. Client-rendered content won't be captured, and geometry-dependent rules report notApplicable (there's no real CSS layout under jsdom).
For client-rendered pages, drive a real browser and call the engine directly against the live DOM — see INTEGRATION.md Pattern 2, or use one of the ready-made bindings (Playwright, Puppeteer, Cypress, Selenium, WebdriverIO).
Common uses
# Target a conformance level
surea11y scan ./dist/index.html --tags wcag2a,wcag2aa
# Machine-readable output
surea11y scan ./dist/index.html --json > result.json
# Accept today's violations, gate CI only on new ones
surea11y scan ./dist/index.html --write-baseline baseline.json # once, commit it
surea11y scan ./dist/index.html --baseline baseline.json # in CI, from then on
# Browsable HTML report, and SARIF for GitHub Code Scanning
surea11y scan ./dist/index.html --html report.html --sarif results.sarif
# Your own org-specific rules
surea11y scan ./dist/index.html --custom-rules ./a11y-rules.jsFull flag reference, custom-rule contract, and CI recipes: docs/CLI.md.
Do you need this package?
You need it when you want a command instead of code. Two examples.
You want the CLI
A marketing site built to static HTML. The CI step is one shell line; there's no test file anywhere.
# .github/workflows/a11y.yml
- run: npm run build
- run: npx @surea11y/cli scan ./dist/index.html --tags wcag2a,wcag2aaThe scan exits 1 if anything fails, which fails the step. You wrote no JavaScript, so you needed the command.
You don't
A dashboard whose chart only exists after you click "Load report". You're already writing a Playwright test, so call the engine directly:
const { A11yCoreBuilder } = require('@surea11y/playwright');
test('dashboard is accessible after loading', async ({ page }) => {
await page.goto('/dashboard');
await page.getByRole('button', { name: 'Load report' }).click();
const results = await new A11yCoreBuilder({ page }).analyze();
expect(results.checksResults.filter((r) => r.outcome === 'fail')).toEqual([]);
});No @surea11y/cli here. This is also CI — but the CLI would be useless for it, because it never executes page JavaScript. Point surea11y scan at that URL and it sees the page before the click ever happens: no chart, nothing to check.
The dividing line
It isn't local vs. CI — both examples are CI. It's:
- Is what you want to scan present in the HTML the server sends? The CLI works, and it's the cheap path: no browser to launch.
- Does it only appear after JavaScript runs? You need a real browser, so you need the library or a binding, and the CLI can't help.
That second case is why "use the CLI for pipelines" is a misleading shorthand — plenty of pipelines are the second example.
Either way you're running the same engine. @surea11y/core has zero runtime dependencies and evaluates whatever DOM you hand it, so if you already have a browser or a jsdom instance, you don't pay for jsdom twice. This package is the one that brings jsdom along, because turning fetched HTML into a DOM is what a CLI has to do. Splitting the two is what keeps that promise true.
Part of the SureA11y family
One engine, several front ends. If the dividing line above pointed you away from
the CLI, install the one matching your setup instead — each pulls in
@surea11y/core for you.
| Your setup | Install |
|---|---|
| Playwright | @surea11y/playwright |
| Puppeteer | @surea11y/puppeteer |
| Selenium | @surea11y/selenium |
| Cypress | @surea11y/cypress |
| WebdriverIO | @surea11y/webdriverio |
| Jest or Vitest component tests | @surea11y/test-matchers |
| Your own script, against a DOM you already have | @surea11y/core |
Local development
npm install
npm testTo run against a local, unpublished @surea11y/core — when you're changing the
engine and the CLI together — pack it and install it without saving, so
package.json keeps declaring its normal semver range:
(cd ../core && npm pack --pack-destination /tmp)
npm install /tmp/surea11y-core-<version>.tgz --no-save --no-package-lock
npm testNever commit a file: dependency. Note that npm link is not a substitute
here: it exposes the whole working directory and so ignores core's files
allowlist, which can resolve paths that won't exist in the published package.
See RELEASE.md for the publish checklist and the cross-repo
ordering constraint.
License
MIT. The engine, @surea11y/core, is MPL-2.0.
