CLI reference
manni a11y exposes one subcommand, check. It crawls a site
from one or more seed URLs and runs axe-core against every page it reaches.
There is no default subcommand: manni a11y <url> is a usage error, and the
verb is always spelled.
manni a11y [options] check [urls...] [command options]Global options
Section titled “Global options”Global options are accepted before the subcommand.
| Option | Description |
|---|---|
-V, --version |
Print the manni version and exit. |
-h, --help |
Print help for the program or a subcommand. |
The accessibility tool. a11y accepts its own -V, --version (the same
version) and -h, --help. Run with no subcommand it prints a usage error and
(add --help for usage), exit 2.
manni a11y [options] check [urls...] [command options]Options
Section titled “Options”| Option | Description |
|---|---|
--no-color |
Disable colored output. Color is on only when stdout is a TTY, and never under NO_COLOR. The meanings match manni meta’s: ✓ green, ✗ red, severity from dim through yellow to red, URLs cyan. |
a11y check
Section titled “a11y check”Crawl a site from the given seeds, or from a collection’s url: or a11y.urls
in config, and run axe-core against every page. Exit 1 when any page keeps a
violation at or above the severity floor.
manni a11y check [urls...] [options]Arguments
Section titled “Arguments”| Argument | Description |
|---|---|
[urls...] |
Seed URLs, space-separated, each http: or https:. Every seed’s host is in crawl scope. Optional, and falls back to a collection’s url: or config a11y.urls; none of the three is an error (exit 2). See where seeds come from. A seed that is not http(s) is rejected before any browser launches. |
Options
Section titled “Options”| Option | Argument | Default | Description |
|---|---|---|---|
-f, --format |
<pretty|json|github> |
pretty |
Output format. An unknown value is an error (exit 2). |
--no-crawl |
n/a | on | Check exactly the given URLs, with no sitemap lookup and no link following. Crawling is on by default. Overrides config crawl:. |
--max-pages |
<n> |
Cap on pages checked. Whatever is still queued at the cap is reported as skipped. Without it every discovered page is checked. Must be an integer of 1 or more (exit 2 otherwise). Overrides config maxPages:. |
|
--tags |
<list> |
Comma-separated axe tags, given once; restrict axe to rules carrying any of them, for example wcag2a,wcag2aa,wcag21aa,best-practice. Whitespace around commas is trimmed and empty items are dropped. A second --tags replaces the first. Without it axe runs its default rule set. Overrides config tags:. |
|
--severity |
<level> |
notice |
Minimum severity reported and counted, on the family scale: notice, warning, or error. Violations below it are dropped before the score and the exit code are decided. An unknown level is an error (exit 2). axe’s four impact levels are folded onto these three when a page is read. Every finding keeps axe’s own value as impact, and the github format has the table. Overrides config severity:. |
--timeout |
<ms> |
30000 |
Per-page navigation timeout in milliseconds. Overrides config timeout:. |
-q, --quiet |
n/a | off | In pretty output, hide pages with no remaining violations. Has no effect on json or github. |
--progress |
n/a | auto |
Report progress on stderr while the crawl runs. The default is on only when stderr is a terminal. Pass it to see each page in a CI log. |
--no-progress |
n/a | off | Never report progress, even on a terminal. |
--collection |
<name> |
every collection | Seed from the url: of a configured collection. Repeatable, one name per occurrence, never comma-separated; repeats collapse. Cannot be combined with positional URLs, needs a config file, and a named collection that declares no url: is an error (exit 2 for all three). See where seeds come from. |
-c, --config |
<path> |
discovered | Path to a config file. When set, the file must exist or the run exits 2. |
--no-config |
n/a | off | Ignore any discovered config file. -c and --no-config set the same option, so the one written later on the command line wins. |
Where seeds come from
Section titled “Where seeds come from”Four sources can name a seed. The first non-empty one wins, and the rest are ignored:
- Positional
[urls...], as typed. - The
url:of each collection named by--collection, in the order the collections are declared. a11y.urlsin config.- The
url:of every declared collection that has one, in declaration order.
a11y.urls sits ahead of the collections because a site has entry points no
documentation collection covers, such as a marketing or status page. Nothing
that already sets a11y.urls changes behaviour by adding a collection url:.
Naming a collection that declares no url: is an error, while a collection
without one simply contributing nothing to rung 4 is not. --collection blog
asked for something specific and has to be told it cannot be done; a bare
manni a11y check gets whatever is publishable.
manni: collection "blog" has no url: to check.manni: --collection selects a configured collection; it cannot be combined with URLs.manni: --collection needs a config file to select from.manni: No URLs to check. Pass one or more, set url: on a collection, or set a11y.urls in manni.config.yaml.All four exit 2.
The crawl
Section titled “The crawl”Scope is the host. Two URLs are on the same site when their hostnames match
case-insensitively and their non-default ports match. http:// and https://
of one host are one site, and www.example.com is not example.com. Each seed
adds its own host to the scope.
The frontier is filled in this order:
- Seeds, as given, normalized (fragment dropped, hostname lowercased, default port dropped, path and query kept as written).
- The sitemap. One lookup per run, from the first seed. The candidates, in
order, are every
Sitemap:line in<origin>/robots.txt, in the order the file lists them, followed by a walk of directories. The walk goes from the seed’s own directory up to the site root, nearest directory first and the root last. In each directory it triessitemap.xml,sitemap-index.xml, andsitemap_index.xml. For a seed ofhttps://example.com/a/b/page.htmlthat is/a/b/, then/a/, then/, three names in each. The first candidate that returns2xxand parses is used, and nothing after it is fetched. A<sitemapindex>is followed into its child sitemaps (depth 3, 50 children at most); a<urlset>contributes its<loc>values. Only same-host page URLs are kept. A missing, failing, or unparseable sitemap means “no sitemap” and is never an error. Gzipped sitemaps are not read. - Links. After each page loads, the absolute
hrefof everya[href]in the rendered DOM is queued if it is same-host,http(s), and looks like a page.mailto:,tel:,javascript:, anddata:are skipped, and so is a path whose last segment ends in an asset extension (.pdf,.zip,.png,.jpg,.svg,.css,.js,.json,.xml,.txt,.mp4,.woff2, among others).
Each normalized URL enters the frontier once, and a trailing slash does not make
a second page. /install and /install/ are one entry, under whichever
spelling arrived first. Where the browser lands after a redirect counts as
visited too. A queued spelling of that page is then dropped without loading and
reported as a duplicate. Pages are analyzed sequentially, in discovery order,
and every result records its source: seed, sitemap, or link. The run
goes on until the frontier is empty. With --max-pages, it stops once that many
pages have been checked, and the frontier left behind is the skipped count.
Failures split by where they happen. A seed that fails to load or analyze stops
the run with exit 2, because nothing was checked. A crawled page that fails is
recorded with an error on that page, the crawl continues, and the page counts
as failed for the exit code.
The score
Section titled “The score”score = round(100 × passes ÷ (passes + violations))passes is the number of axe rules that ran and passed on the page.
violations is the number of rules that failed at or above the --severity
floor, counted per rule rather than per element. The result is the share of
applicable rules that passed after severity filtering. It is not a Lighthouse
score: nothing is weighted, and a page with one error still fails the run
whatever its score says. The score is null when the page did not load
or when no rule applied.
Rules axe could not decide are reported as incomplete and never counted as
failures.
Output formats
Section titled “Output formats”pretty (the default) is for people. It prints a header, one line per page,
the violations under each failing page, and a footer:
Checked 3 of 3 pages (sitemap: http://localhost:8080/sitemap.xml)✓ http://localhost:8080/index.html score 100✗ http://localhost:8080/about.html score 71 2 errors, 1 notice error image-alt (axe: critical) 3 nodes Images must have alternate text https://dequeuniversity.com/rules/axe/4.13/image-alt img.hero → Fix any of the following: Element does not have an alt attribute; … img.map → Fix any of the following: Element does not have an alt attribute; … img.logo → Fix any of the following: Element does not have an alt attribute; … error html-has-lang (axe: serious) 1 node <html> element must have a lang attribute https://dequeuniversity.com/rules/axe/4.13/html-has-lang html → Fix any of the following: The <html> element does not have a lang attribute notice region (axe: minor) 1 node All page content should be contained by landmarks https://dequeuniversity.com/rules/axe/4.13/region p → Fix any of the following: Some page content is not contained by landmarks✗ http://localhost:8080/orphan.html could not load: net::ERR_CONNECTION_REFUSED4 violations on 2 of 3 pagesThe header names the sitemap that supplied pages, or reads (no sitemap; followed links). Under --no-crawl it reads (no crawl). Each violation line
carries the severity, the rule id, and axe’s own impact in parentheses. Then
come the node count, axe’s short help text, and the rule’s Deque University
URL. The severity is what the floor and the exit code use. The impact is there
for lookup, since the Deque page and the axe DevTools report both name it.
Under it, the first three failing elements print as
a selector and axe’s failure summary, then (+N more) past three. A page that
could not load prints could not load: <reason>. The footer adds
; K skipped (--max-pages) when the cap was hit, and ; K duplicates dropped
when a redirect landed on a page that was also queued under another spelling.
-q hides pages with no remaining violations.
json prints one object, the whole run, for scripts:
{ "results": [ { "url": "https://docs.example.com/install/", "source": "sitemap", "violations": [ { "id": "image-alt", "severity": "error", "impact": "critical", "help": "Images must have alternate text", "helpUrl": "https://dequeuniversity.com/rules/axe/4.13/image-alt", "tags": ["cat.text-alternatives", "wcag2a", "wcag111"], "nodes": [ { "target": "img.hero", "html": "<img src=\"hero.png\" class=\"hero\">", "summary": "Fix any of the following:\n Element does not have an alt attribute" } ] } ], "passes": 7, "incomplete": 2, "score": 88 } ], "summary": { "discovered": 42, "checked": 42, "skipped": 0, "duplicates": 0, "failed": 2, "violations": 3, "bySeverity": { "notice": 0, "warning": 1, "error": 2 }, "sitemap": "https://docs.example.com/sitemap.xml", "crawl": true }}Each entry of results[]:
| Field | Type | Meaning |
|---|---|---|
url |
string | The normalized URL that was checked. |
source |
"seed" | "sitemap" | "link" |
How the URL entered the run. |
violations |
array | Rules that failed at or above the severity floor. Each has id, severity, impact, help, helpUrl, tags, and nodes[] with target (axe’s CSS selector path), html (the element’s outer HTML), and summary (axe’s failure summary). Empty when error is set. |
violations[].severity |
"notice" | "warning" | "error" |
The family level, mapped from impact. What the floor, the exit code, and the github format use. |
violations[].impact |
"minor" | "moderate" | "serious" | "critical" |
axe’s own value, kept for lookup against its rule pages. A rule axe reports with no impact is recorded as "minor". |
passes |
integer | Rules that passed on this page. |
incomplete |
integer | Rules axe could not decide. Reported, never counted as failures. |
score |
integer | null |
See the score. null when the page did not load or no rule applied. |
error |
string, optional | Present when the page could not be loaded or analyzed. The page counts as failed. |
And summary:
| Field | Type | Meaning |
|---|---|---|
discovered |
integer | Distinct URLs that entered the frontier, whether seeds, sitemap entries, or links. |
checked |
integer | Pages actually analyzed. |
skipped |
integer | Discovered but not checked because --max-pages was reached. 0 when no cap is set. |
duplicates |
integer | Discovered, then dropped without loading because a redirect had already landed on the same page under another spelling. checked + skipped + duplicates equals discovered. |
failed |
integer | Pages with at least one remaining violation or an error. Drives exit 1. |
violations |
integer | Remaining violations across all pages, counted per rule rather than per node. |
bySeverity |
object | Violation counts keyed notice, warning, error. All three keys are always present. |
sitemap |
string | null |
The sitemap URL that supplied pages, or null when none was used. |
crawl |
boolean | Whether pages beyond the seeds were discovered. false under --no-crawl. |
github prints one workflow command per finding, and nothing when the run
is clean:
::error title=a11y/image-alt::Images must have alternate text — 2 nodes on https://docs.example.com/install/ (https://dequeuniversity.com/rules/axe/4.13/image-alt)::warning title=a11y/landmark-unique::Ensures landmarks are unique — 1 node on https://docs.example.com/install/ (https://dequeuniversity.com/rules/axe/4.13/landmark-unique)::error title=a11y/load::https://docs.example.com/old/: net::ERR_NAME_NOT_RESOLVEDGitHub renders each as an annotation on the workflow run. There is no file or
line to anchor to, since the finding is about a URL. The command’s level is the
finding’s severity. The family scale was chosen to match GitHub’s, so the
annotations are one-to-one. axe’s four impact levels were folded onto that scale
when the page was read, and this is the map. A page that did not load is always
error.
| axe impact | Severity | Annotation |
|---|---|---|
critical |
error |
::error |
serious |
error |
::error |
moderate |
warning |
::warning |
minor |
notice |
::notice |
Progress
Section titled “Progress”The report waits for the whole crawl, and a fifty-page site takes a minute. So
check narrates the run on stderr while it works. It says when the browser is
starting, which sitemap it found, and which page it is on, as
[3/51] https://docs.example.com/install/. On a terminal that is one status
line, rewritten in place and cleared before the report prints. Off a terminal
it is one line per event, each prefixed manni: , with no escape codes.
Progress is on by default only when stderr is a terminal, so a CI log stays as
it was. Pass --progress to see it in a CI log anyway. Pass --no-progress
to silence it. Either way stdout carries the report alone, so
-f json | jq keeps working.
Exit codes
Section titled “Exit codes”| Code | Meaning |
|---|---|
0 |
Every page loaded and none has a violation at or above the severity floor. |
1 |
At least one page has a remaining violation, or a crawled page failed to load. summary.failed is greater than zero. |
2 |
Usage or operational error. The causes are an unknown flag value, a non-http(s) URL, or no URLs and no config. They also include a seed that could not be loaded, or no browser found. One line on stderr prefixed manni: . |
Examples
Section titled “Examples”Get started covers the first
three rungs, a bare crawl, --no-crawl, and the zero-argument config form.
Continuing from there:
# Several seeds: each seed's host is in scope; one sitemap lookup, from the first seed.manni a11y check https://docs.example.com/ https://blog.example.com/
# One collection, seeded from its url: — the same name meta validates by.manni a11y check --collection guides
# Two of them, in the order the config declares, whatever order they are named.manni a11y check --collection blog --collection guides
# Gate only on what matters: WCAG 2.1 AA rules, errors only, hide clean# pages. Exit 1 only if an error remains; warnings and notices are dropped.manni a11y check https://docs.example.com/ --tags wcag2a,wcag2aa,wcag21aa --severity error -q
# CI: GitHub annotations, bounded run, explicit config, no color.manni a11y check https://docs.example.com/ -f github --max-pages 50 -c ci/manni.config.yaml --no-color
# Scripts: the JSON envelope, piped to jq.manni a11y check https://docs.example.com/ -f json | jq '.summary'
# Every option at once. Two seeds (space-separated); sitemap and links; cap# 250 pages; one comma-separated --tags list; warning floor; 60 s per page;# quiet; ignore any discovered config; json; no color.manni a11y check https://docs.example.com/ https://www.example.com/ \ --max-pages 250 \ --tags wcag2a,wcag2aa,wcag21aa,best-practice \ --severity warning \ --timeout 60000 \ --quiet \ --no-config \ --format json \ --no-colorUsage errors are one line on stderr, prefixed manni: , exit 2:
manni a11y# usage error: no subcommand given; commander prints the problem and "(add --help for usage)"
manni a11y check not-a-url# manni: Not an http(s) URL: "not-a-url".
manni a11y check https://x.example/ --severity high# manni: Unknown --severity "high". Use notice | warning | error.
manni a11y check https://x.example/ --max-pages 0# manni: --max-pages must be an integer >= 1.
manni a11y check https://x.example/ -f sarif# manni: Unknown --format "sarif". Use pretty | json | github.
manni a11y check https://x.example/ # with no browser installed# manni: No browser found. Install one with `npx playwright install chromium`, or install Google Chrome or Microsoft Edge.Configuration
Section titled “Configuration”a11y reads its own key of the shared manni.config.yaml, discovered the same
way meta discovers it, walking up from the working directory. -c <path>
names the file explicitly, and --no-config ignores any discovered one. A flag
wins over the same key in config, and config wins over the default. Every key
is optional, and an empty a11y: section is the same as none.
collections: - name: site paths: ["docs/"]
a11y: urls: ["https://docs.example.com/"] crawl: true maxPages: 500 tags: ["wcag2a", "wcag2aa"] severity: error timeout: 30000| Key | Type | Default | Description |
|---|---|---|---|
urls |
string[] |
none | Seed URLs, each http: or https:. A fallback for [urls...]; positional URLs win when both are present, and this key wins over a collection’s url:. See where seeds come from. |
crawl |
boolean | true |
Whether to discover pages from the sitemap and same-host links. --no-crawl sets it to false for one run. |
maxPages |
integer ≥ 1 | none, no cap | Cap on pages checked per run. Without it every discovered page is checked. Mirrors --max-pages. |
tags |
string[] |
none | axe tags to restrict the rule set to. Mirrors --tags, one item per entry. |
severity |
notice | warning | error |
notice |
Minimum severity reported and counted, on the family scale. Mirrors --severity. |
timeout |
integer ≥ 1 | 30000 |
Per-page navigation timeout in milliseconds. Mirrors --timeout. |
An unknown key, a wrong type, an empty urls entry, or a urls entry that is
not http(s) is an error. The error names the file and the key (exit 2).
urls is optional. A repository that declares its documents as
collections can give each
one a url: and leave the a11y: section to the options above, or omit it
entirely:
collections: - name: guides paths: ["docs/guides/**/*.md"] url: https://docs.example.com/guides/
a11y: severity: warning