HTML report
A self-contained HTML report you can open in a browser after a scan — no server, no external assets, no network requests. Built for QA/manual testers who want a browsable view of a scan's results rather than raw JSON or a terminal summary. Open a sample report: a small checkout page scanned with the listener and live-region hooks, with a finding in every group and what the scan leaves to a person.
$ surea11y scan ./dist/index.html --html report.html
Open report.html directly from disk. Works alongside any other output mode — --html doesn't replace --json/the default summary, it's an additional artifact written to the given path.
What it shows
- Hero: one plain-language headline ("N of M applicable checks passed") plus a stacked bar and legend (icon + label + count — status is never color-only) broken down by outcome (
fail/cantTell/pass/notApplicable). When no rule failed and some applied, a note under the headline says that is not a claim the page conforms, and names what a person still checks: the findings that need review, keyboard use, how a screen reader reads the page, and the content itself. - Worth reviewing: one card per rule with
fail/cantTelloccurrences (not one per occurrence — a rule with many identical occurrences is one thing worth attention, not many), each showing severity, WCAG SC chip(s), in a scan of several profiles what each profile makes of it (“wai-aria-1.2: Fail” beside “wcag22-aa: Needs review”, itsverdicts), a representative occurrence, and the total occurrence count. Failing rules come first. The findings that need review follow in three groups: those whose evidence points to a failure (“Likely fails: confirm”), those whose evidence points to a pass (“Likely passes: confirm”), and those only a person can judge (“Needs a person”), asreviewGroupOf(check, occurrence)from@surea11y/corefiles them, so a tool that sorts a review queue sorts it the same way. Each card in the first two says why it leans that way (the occurrence'slean). Each of these cards has a “How to check” section, collapsed by default, with the steps to answer the question and what passes and what fails (the occurrence'showToCheck). A rule whose findings lean different ways has a card in each group. Within each group, most severe first (critical to minor) and, at the same severity, the one with more occurrences first. Capped at the 24 highest-priority rules with an overflow note past that. A rule that did not complete (it threw) gets a card too, saying so with its error, since the page was not checked against it. A rule with anhttp(s)help link (meta.helpUrl, a custom rule's included) gets a "How to fix <rule>" link on its card; a rule without one links the Understanding document of the WCAG criterion it checks ("Understanding 1.1.1 Non-text Content"). - Frames: only for a cross-frame result (
runa11yCoreAcrossFrames, or a binding's.frames(true)). The cards above are the page's own; this section has one heading per frame, named by its path (the selectors of the frame elements leading to it), with the frame's title and URL and its cards, or "Not scanned" and why for a frame that did not answer. The rest of the report describes the page itself. - Closest to the limit: shown when the scan has margins (
marginon a check result). One row per rule whose closest passing element came close to its threshold (within the rule's band, such as 10% of a contrast ratio), as the note above the table says: its closest passing value, the limit, the room left and the element. Pixels are rounded to whole ones, with "less than 1 px" under one; ratios show two decimals. A rule that passed with little room is one a small change can turn into a failure. - WCAG rollup: grouped by conformance level (A / AA / AAA), sourced directly from the engine's own
rulesResults[]composite rollups (see Conformance) — one row per Success Criterion (with the requirements of each other standard in the result'sstandardsthat its rules map to, such as the EN 301 549 clause that restates it, where there is one and the scan asked for EN 301 549 clauses), its outcome, a pass/fail/needs-review/n/a breakdown (with a “Partly checked” line when the criterion's rules check only part of what it asks, so a person checks the rest, as “Partly checked: its checks cover only part of this criterion, so a person checks the rest”, or when the scan selected only some of its rules, as “Partly checked: 1 of 2 checks was not selected for this scan”, see A partly checked criterion), and which atomic rules contributed, a custom rule mapped to the criterion included. This is real engine data, not an invented grouping — the same rollup you'd get from the raw JSON'srulesResults. - A standard's own rollup: only when the scan ran the rules of a standard that has rollups of its own (its profile, or
engineOptions.optInRules), one table per standard, and per version of it when a scan of several profiles judged the page by two (EN 301 549 V3.2.1 with V4.1.1, each named by its version), with one row per rollup, with the standard's wording, the requirements of the rules that decided the outcome, the breakdown and the contributing rules. This covers a pack's standard and a pack's checklist, whose items are its rollups. Each such standard is one listed in the result'sstandards, so a stored result renders the same wherever it is rendered; when that list is missing or damaged, the report says so above the WCAG rollup. - What's left for a person (core 1.15.0 and later): what the scan can't decide in the requirements its rules evaluate. Every requirement of the standard scanned with a check no rule decides in full, each with its outcome in the scan beside it (“Scan: Fail”, “Scan: Pass”, or “No rule tests it”), and under it, collapsed by default, those checks: what each assesses, how much of it rules decide, the tools it needs, the steps, and what passes and what fails (core's test procedures). A sentence above says how many checks and requirements there are, and that a requirement whose rules all pass is met only once its checks pass too. The standard is WCAG, at the version the scan targeted and the levels it reported; where the scan ran a pack's profile and the pack, passed in
options.packs, gives procedures, it is that pack's standard, under its own numbers. A scan of several profiles has a part for each standard and version. A note below says how to make a worksheet of them, to record a result for each (renderProcedures, or the CLI's--manual-checks). It is written in Node; the browser report (surea11y.report.js) has none. - Full technical data (collapsed by default): a scorecard (tiles per outcome) and a searchable, filterable (by outcome), paginated table of every individual occurrence across the whole scan; a finding that needs review and has a lean shows it under its summary (“Likely fails: …”), and the search finds it. When it shows no occurrence, it holds one row saying so: the scan recorded none, or the filters hide them all.
An element inside a shadow tree is located by the shadow hosts' selectors, then its own, joined with >>> : html > body > main > x-card >>> button. The cards, the margins table and the occurrence table all show it that way, as do SARIF and JUnit.
The report follows the reader's color scheme, and its own text meets WCAG 1.4.3 in both: every status color is at least 4.5:1 on its chip and tile, and the engine's own contrast rules pass on the rendered report in light and dark mode.
The report reflows at 320 CSS px (WCAG 1.4.10): the page never scrolls sideways. A table wider than the window scrolls in a box of its own, a region named by the table's heading that takes keyboard focus, so it can be scrolled with the arrow keys.
The meta bar under the header carries the rule count, occurrence count, engine tag, schema version, the WCAG version the scan targeted, the engineOptions.profile it used (when there was one; every profile of a scan of several, joined with +, and those that did not run) and, beside it, the law the profile is assessed under when a pack's profile names one (core 1.16.0, experimental), the opt-in rule tags engineOptions.optInRules added (when it added any), the conditions the page was rendered under, and the locale the scan resolved to. The conditions are the viewport (with the device pixel ratio when it isn't 1) and the color scheme, and the web fonts when they were still loading; a scan with no layout, in jsdom, says so instead. A layout-dependent finding can come and go with the viewport width, so the chip says which width the report describes. Locale fallback is per-string and invisible in the text itself, so a report requested in a language the engine does not carry reads as an ordinary English one — the chip names the requested locale alongside the resolved one when the two differ. See Locales.
The whole page is written in the locale the scan resolved to: headings, table columns, outcome and severity names, the headline, the pager, and the date format. <html lang> names that locale. The outcome codes in the occurrence filters (fail, cantTell, …) stay as they are, because they are the values a reader searches for in the JSON result.
One case keeps English labels: a scan in a language the engine does not ship, run with a caller-supplied engineOptions.messages dictionary. That dictionary is not part of the result, so the report cannot read labels from it; its findings are then marked with their own language (lang="nl", for instance), so a screen reader still reads them with the right voice.
A compact result (engineOptions.output.detail: 'findings', see Output schema) is read as the full result it stands for, so it is reported the same.
Library usage
const { renderHtmlReport } = require('@surea11y/core/report');const { runDomRulesInPage } = require('@surea11y/core');const result = runDomRulesInPage(url, null, {}, null);const html = renderHtmlReport(result, { title: 'My scan report' });require('fs').writeFileSync('report.html', html);
renderHtmlReport(result, options) returns a string and never touches the filesystem itself (the CLI's --html flag does the writing). The page is dated by the result's timestamp (set it with engineOptions.timestamp), shown in UTC, so the same result always renders the same page; a result without one is dated by the time of rendering. options.title is optional (defaults to the report language's own title, "surea11y scan report" in English). options.packs is the list of packs the scan ran (engineOptions.packs), when it ran any: in Node the report adds the “How to check” steps to undecided findings that come without them, a core rule's and a variant's base rule's always, and a pack rule's own steps (its meta.review, in English, or as its pack's dictionaries translate them) only when given the packs (see Packs). In the browser (surea11y.report.js) it shows the steps the result already carries. With the packs, “What's left for a person” also follows a pack's standard where the scan ran its profile. options.manualChecks: false leaves that section out.
Scope
This is a single-scan report — one point-in-time snapshot, not a dashboard tracking results across many scans over time. Tracking results across many runs is a separate, larger concern and isn't part of this tool: keep the JSON from each scan and diff it yourself, or feed SARIF to a dashboard that already does history (see SARIF report below).