Usage via CLI

A command instead of code — no test framework or Node script required, good for a pipeline step or a quick check against a build artifact.

First scan

$ npm install -g @surea11y/cli
$ surea11y scan ./index.html
$ surea11y scan https://example.com/ --wcag 2.2

Requires Node.js ^20.19.0 || ^22.13.0 || >=24.0.0. The summary names the @surea11y/core release that ran the scan:

$ surea11y scan ./index.html
surea11y scan: file:///path/to/index.html
engine: @surea11y/core 1.16.1
pass: 16 fail: 4 cantTell: 4 notApplicable: 121
occurrences by tier: fail: 4 cantTell: 4
needs review: likely fail: 1 likely pass: 0 needs a person: 3
FAIL (4 rule(s)):
button-name-present (serious, 1 fail occurrence(s))
- html > body > main > button
This button has no accessible name.
hint: Give the button visible text, or name it with aria-label, so screen
reader users know what it does.
contrast-enhanced (serious, 1 fail occurrence(s))
- html > body > main > p
Element has insufficient color contrast (AAA) of 2.85:1 (foreground:
#999999, background: #ffffff, font size: 16px, font weight: normal).
Expected contrast ratio of 7:1 (normal text).
hint: Change the text color, the background color, or both, so the
contrast ratio reaches at least 7:1.
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: 16px, font weight: normal). Expected
contrast ratio of 4.5:1 (normal text).
hint: Change the text color, the background color, or both, so the
contrast ratio reaches at least 4.5:1.
img-alt-present (serious, 1 fail occurrence(s))
- html > body > main > img
This <img> has no alt attribute.
hint: Add an alt attribute that says what the image shows, or alt="" if
the image is purely decorative.
LIKELY FAIL — confirm (1 occurrence(s)):
- heading-order (minor): html > body > main > h3
This heading jumps from level 1 to level 3, skipping a level.
why: The heading skips a level after the one before it.
cantTell — needs human review (4 rule(s)): heading-order, link-name-quality,
manual-review, page-title-patterns
criteria (WCAG 2.2):
fail 1.1.1 Non-text content: text alternatives; 1.4.3 Contrast: minimum;
1.4.6 Contrast: enhanced; 4.1.2 Name, role, value: accessible name
cantTell 1.2.1 Audio-only and video-only (prerecorded): transcript; 1.2.2
Captions (Prerecorded); 1.3.1 Info and Relationships; 1.3.4
Orientation; 1.3.5 Identify Input Purpose; 1.4.1 Use of Color;
1.4.2 Audio Control; 1.4.4 Resize Text; 1.4.5 Images of Text;
1.4.12 Text Spacing; 2.1.1 Keyboard; 2.1.3 Keyboard (No Exception);
2.2.1 Timing Adjustable; 2.2.2 Pause, Stop, Hide; 2.2.4
Interruptions; 2.4.1 Bypass Blocks; 2.4.2 Page titled; 2.4.3 Focus
order; 2.4.4 Link Purpose (In Context); 2.4.6 Headings and Labels;
2.4.7 Focus visible; 2.4.9 Link Purpose (Link Only); 2.5.8 Target
size: minimum; 3.1.1 Language of page; 3.1.2 Language of Parts;
3.2.5 Change on Request; 3.3.1 Error Identification; 3.3.2 Labels
or Instructions; 3.3.8 Accessible Authentication (Minimum); 4.1.2
Name, role, value: ARIA validity; 4.1.3 Status Messages
notApplicable: 2
A criterion that passes here means the checks that ran found nothing in it,
not that the page conforms.

The needs review line sorts the questions by what a reviewer does with them: likely fail (the page's evidence points to a failure, as with the skipped heading level here), likely pass, and needs a person. The likely failures are listed with the reason each leans that way, and every question in --json and in the reports carries the steps to answer it (howToCheck).

The criteria block reads the same scan by WCAG criterion, the way people usually ask ("does it fail 1.4.3?"): fail first, then cantTell, and a count of notApplicable. Since @surea11y/core 1.14 no criterion is listed as pass: one passes only when its rules check everything it asks, and none is listed as checked in full yet, so where its rules all pass it's cantTell, for a person to check the rest. The block never changes the exit code, and --compact shows it as counts. With no failing rule, the summary ends by saying that this is no claim the page conforms, and names what a person still checks.

Several pages, or a sitemap

Name several pages, list them in a file (one path or URL per line, with # comments), or point the scan at a sitemap:

$ surea11y scan ./dist/index.html ./dist/pricing.html
$ surea11y scan --urls pages.txt
$ surea11y scan --sitemap https://example.com/sitemap.xml # up to 50 pages; --max-pages to change it

Each page gets its own section of the summary, a last line counts the pages with failures, and the run fails when any page does. --json prints an array, --sarif and --junit write one file covering every page, --html writes a report per page (report.html, report-2.html…), and a baseline remembers which page each known failure was on. A sitemap can be a URL or a local file, plain or gzipped, and a sitemap index is followed one level down. A page that can't be scanned stops the run with exit 2, naming it, before anything is written.

In a real browser

By default the CLI reads static HTML. For pages whose content their JavaScript renders, install Playwright in your project and add --browser: each page opens in headless Chromium, its scripts run, and the scan sees what renders, with a real layout for the rules that measure it. The engine is the same @surea11y/core, and every option works with it, reports, baselines, packs and custom rules included.

$ npm install --save-dev playwright
$ npx playwright install chromium
$ surea11y scan https://app.example.com/ https://app.example.com/settings --browser

An app that fetches its data after loading can make the scan wait for it, and a page can be scanned at a phone's width, in dark mode, or in every combination of several:

$ surea11y scan https://app.example.com/orders --browser --wait-for "#orders-table"
$ surea11y scan https://example.com/ --browser --width 320 --color-scheme dark
$ surea11y scan https://example.com/ --browser --width 1280,320 --color-scheme light,dark

--wait-for waits up to 30 seconds (--wait-timeout changes that) and stops the run with exit 2 if the element never shows. Several widths or color schemes load the page once and merge the scans into one result per page: a rule's worst outcome, and each finding once, with the combinations it was seen in (seen in:).

Before each page loads, the CLI installs core's internals and listener hooks, so the scan judges names that web components set through ElementInternals, and click handlers added from script: a role="button" that Enter and Space never reach fails. --live-regions adds the live-region hook, so a status message added with its text already in it, which screen readers commonly don't announce, is asked about; --no-hooks leaves the first two out. The summary names the viewport, color scheme and hooks each scan used. The CLI doesn't click, type or log in: for content that only appears after that, drive the browser with one of the bindings. Frames inside the page aren't scanned yet.

Choosing what runs

--wcag selects the rules for a WCAG conformance target, by the level each criterion has in that version; --wcag-level sets its level (AA unless it says otherwise). --best-practices adds the rules that test no WCAG criterion, such as region and heading-order; on its own it runs only those. --profile runs a conformance profile, such as en301549-v4.1.1 or a pack's own.

$ surea11y scan ./dist/index.html --wcag 2.2 # WCAG 2.2, Level A and AA
$ surea11y scan ./dist/index.html --wcag 2.1 --wcag-level A # WCAG 2.1, Level A
$ surea11y scan ./dist/index.html --wcag 2.2 --best-practices # and the best practices

4.1.1 Parsing, which WCAG 2.2 removed, is part of a 2.1 target and not of a 2.2 one. --rules and --tags add to a target's rules, and --exclude-rules applies after. Beside --profile, --best-practices adds its rules and the profile still applies; --wcag, --rules and --tags choose the rules in a profile's place, so --profile with any of them exits 2. These map onto runOnly.wcag, runOnly.bestPractices and engineOptions.profile (see Engine options).

Naming the checks yourself

Given both, --rules and --tags run the rules that match both. To say "this area, plus these checks" instead, pass --include-mode or, which runs the rules that match either. Without it, the scan below runs no rule, since contrast-computable isn't tagged forms, and exits 2 rather than pass on nothing.

$ surea11y scan ./dist/form.html --tags forms --rules contrast-computable --include-mode or

A scan that names checks reports the WCAG criteria they belong to, at every level. contrast-computable checks 1.4.3 (AA) and 1.4.6 (AAA), so a site that meets AA can show a failing AAA criterion it never aimed for. Without --wcag, --wcag-level sets the level of the criteria reported, without changing which rules run:

$ surea11y scan ./index.html --rules contrast-computable --wcag-level AA
surea11y scan: file:///path/to/index.html
engine: @surea11y/core 1.16.1
WCAG level reported: AA
pass: 1 fail: 0 cantTell: 0 notApplicable: 0
occurrences by tier: fail: 0 cantTell: 0
criteria (WCAG 2.2 AA):
cantTell 1.4.3 Contrast: minimum (partly checked: 1 of 2 checks not selected)
A criterion that passes here means the checks that ran found nothing in it,
not that the page conforms.
No rule failed, which is not a claim that the page conforms: the automated rules
found nothing definite. A person still checks the cantTell findings, keyboard
use, how a screen reader reads the page, and the content itself.

1.4.3 is partly checked: its other check, contrast-minimum, wasn't selected, so its outcome covers only the check that ran, and since no rule failed, the summary ends with the reminder that this is no claim the page conforms. Rule ids and levels are read in any case (--rules IMG-ALT-PRESENT, --wcag-level aa). --include-mode maps onto engineOptions.includeMode, and --wcag-level without --wcag onto engineOptions.wcagLevel.

Packs

--pack loads a pack (rules, a standard or a checklist, and their profiles) by package name, resolved from the working directory's project, or by path; --profile runs one of its profiles. Repeat --pack to scan with several.

$ surea11y scan ./dist/index.html --pack @acme/a11y-policy --profile acme-web-2026
$ surea11y scan ./dist/index.html --pack ./acme-policy-pack.js --profile acme-web-2026

The summary names the profile and the packs that ran, and --json has them as engine.profile and engine.packs. A pack that can't be found or loaded, or that the engine skips, exits 2 with no report written, so a scan never passes with the pack's checks unrun.

Several profiles

--profile can be given more than once, to judge each page by several profiles at once, a pack's beside WCAG's or another pack's (@surea11y/cli 1.5.0, core 1.15.0 and later):

$ surea11y scan ./dist/index.html --pack @surea11y/pack-wai-aria --profile wcag22-aa --profile wai-aria-1.2

Each profile judges the page as it would alone, and the result reads as one profile with the rules of all: a rule's outcome is the worst its profiles give it, so a failure under any profile exits 1, and each finding's verdicts in --json say what each profile makes of it (see Output schema). The summary lists them (profiles: wcag22-aa, wai-aria-1.2), --html names each and shows each one's verdict on a finding, and --sarif and --junit gate on the worst outcome. A profile that didn't run exits 2, listed with why; --allow-profiles-not-run makes it a warning, except for a profile whose pack the engine skipped, since the skipped pack exits 2 itself.

Failing only on serious problems

--fail-on-severity (critical, serious, moderate or minor) fails the run only on failing rules at that severity or above; with --baseline, only on new ones. Every failure is still reported, and the summary marks those below the level, so a team can start by blocking on serious problems and tighten the gate as the backlog shrinks.

$ surea11y scan --urls pages.txt --fail-on-severity serious

Questions that likely fail

--fail-on-lean also fails the run on the questions that likely fail (core's leaningFailures()), at or above --fail-on-severity's level when you give one; --lean-fail-wcag-only counts only those of rules mapped to a WCAG criterion, leaving out the best practices. With it, --junit reports such a rule as a failing test and --sarif such a finding as an error, and --write-baseline records today's likely failures too, so --baseline gates only on new ones. It's opt-in because a lean is evidence, not proof: whether a finding leans, and which way, may change in a minor release of core.

$ surea11y scan ./dist/index.html --fail-on-lean --lean-fail-wcag-only
$ surea11y scan ./dist/index.html --fail-on-lean --write-baseline baseline.json # once, commit it
$ surea11y scan ./dist/index.html --fail-on-lean --baseline baseline.json # in CI, from then on

Manual checks

A scan decides what rules can decide. --manual-checks <path> writes, next to it, the rest: a worksheet of the checks no rule decides in full, for a person to do by hand, with the steps for each, what passes and what fails, and a result and notes to record (core's test procedures). With --html, whose report now ends with “What's left for a person”, the team has both halves of the assessment from one run:

$ surea11y scan https://example.com/ --wcag 2.2 --html report.html --manual-checks manual-checks.html
  • For which standard: the one scanned. A pack's profile gives that standard and version, under its own numbers, where the pack gives procedures; otherwise WCAG, at the version and level of --wcag and --wcag-level, or of --profile's target, and WCAG 2.2 A and AA by default. With several --profile, one worksheet per standard and version.
  • In which language: --locale's. A path ending in .md writes Markdown instead, with a task item per check.
  • Several pages: one worksheet per page, numbered as --html's are, each titled with its page, so each page's results stay apart.

It never changes the exit code. Open the worksheet from disk, record each result and notes, and download them as JSON, with each criterion's result rolled up from its checks, or as CSV.

Procedures, with no scan

surea11y procedures writes how a person assesses each requirement of a standard by hand, with no scan: a reference to read, or a worksheet to work through (--checklist), in HTML, Markdown or JSON.

# How to assess every WCAG 2.2 A and AA criterion, as a page
$ surea11y procedures --wcag 2.2 --wcag-level AA -o wcag-22-aa.html
# A worksheet for a manual audit, titled for it
$ surea11y procedures --wcag-level AA --checklist --title "Audit: example.com" -o audit.html
# Only what no rule decides in full, as Markdown for a repository
$ surea11y procedures --wcag-level AA --automation manual,partial -o left-to-a-person.md
# What needs a screen reader, for a session with a person who uses one
$ surea11y procedures --wcag-level AA --tools screen-reader -o screen-reader-session.html
# One criterion, in French, with the steps a scan's questions give
$ surea11y procedures 1.4.12 --locale fr --variants -o 1.4.12.html
# A pack's standard, under its own numbers
$ surea11y procedures --pack @surea11y/pack-en301549 --profile en301549-v4.1.1 --checklist -o en-301-549.html

--standard and --standard-version name a pack's standard by key and version, --rules keeps only what some rules test, --no-record gives a worksheet with nothing to record, and --json the data instead of a page. A scan's options are a usage error here, as these are on scan. See the CLI reference on GitHub for every flag.

Compact results

--compact keeps each pass and notApplicable result to its rule, outcome and type (engineOptions.output.detail: 'findings'): a smaller --json, for a result kept as a CI artifact. The summary, the exit code, the baseline and the HTML, SARIF and JUnit reports are the same as without it.

$ surea11y scan ./dist/index.html --compact --json > result.json

Exit codes and limitations

cantTell doesn't affect the exit code unless you pass --fail-on-lean: 0 clean, 1 at least one — or, scanning with --baseline, one new — fail on some page (the CI-gating case; --fail-on-severity counts only those at its level or above; see Baseline / allowlist for what counts as "known" vs. "new"), 2 a usage or scan error, or a scan that would have checked nothing or less than asked: a --rules/--tags list naming no known rule or tag, a --context selector that matches nothing, a custom rule or a pack that didn't run, a --profile that wasn't applied (with several, one that didn't run, unless --allow-profiles-not-run). A --baseline or --migrate-baseline file with an entry that isn't one (null, no ruleId, a selector that isn't a string) exits 2 before the scan, naming the file and the entry (@surea11y/cli 1.6.0 and later). A baseline written by the minor release before keeps matching findings whose reason codes moved since; --migrate-baseline baseline.json rewrites it under the new codes (see Upgrading).

Without --browser, the CLI reads static HTML only — a local file, or the raw body of an HTTP(S) GET — and never executes page JavaScript. Neither mode clicks, types or logs in: for content that only appears after that, use one of the bindings, or call the engine directly against a browser you drive (Pattern 2 in INTEGRATION.md, the integration guide on GitHub).

Full flag reference, custom-rule contract, packs and CI recipes: docs/CLI.md. For CI-pipeline templates specifically, see CI integration.