Skip to content

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.

Terminal window
manni a11y [options] check [urls...] [command 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.

Terminal window
manni a11y [options] check [urls...] [command 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.

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.

Terminal window
manni a11y check [urls...] [options]
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.
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.

Four sources can name a seed. The first non-empty one wins, and the rest are ignored:

  1. Positional [urls...], as typed.
  2. The url: of each collection named by --collection, in the order the collections are declared.
  3. a11y.urls in config.
  4. 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.

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:

  1. Seeds, as given, normalized (fragment dropped, hostname lowercased, default port dropped, path and query kept as written).
  2. 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 tries sitemap.xml, sitemap-index.xml, and sitemap_index.xml. For a seed of https://example.com/a/b/page.html that is /a/b/, then /a/, then /, three names in each. The first candidate that returns 2xx and 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.
  3. Links. After each page loads, the absolute href of every a[href] in the rendered DOM is queued if it is same-host, http(s), and looks like a page. mailto:, tel:, javascript:, and data: 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.

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.

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_REFUSED
4 violations on 2 of 3 pages

The 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_RESOLVED

GitHub 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

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.

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: .

Get started covers the first three rungs, a bare crawl, --no-crawl, and the zero-argument config form. Continuing from there:

Terminal window
# 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-color

Usage errors are one line on stderr, prefixed manni: , exit 2:

Terminal window
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.

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.

manni.config.yaml
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