wcag-compliance-reporter
Builds a per-page WCAG 2.2 compliance score report by aggregating output from one or more accessibility scanners (axe-core / pa11y / lighthouse / WAVE / IBM Equal Access), pivoting violations by Success Criterion (1.4.3 contrast, 2.4.7 focus visible, etc.), grouping by conformance level (A / AA / AAA), reporting per-page coverage gaps explicitly (the "this page wasn't scanned" failure mode), and emitting both an executive summary and a per-page drill-down. Use after a multi-page accessibility scan - pa11y-ci, axe across a sitemap, lighthouse-batch - when the team needs a shareable conformance report rather than a per-page tool dump.
Install with skills.sh (any agent)
npx skills add testland/qa --skill wcag-compliance-reporterwcag-compliance-reporter
Overview
WCAG 2.2 has 77 success criteria (SC) split across three conformance levels: 25 at A, 24 at AA, 28 at AAA (wcag-spec (opens in new window)) - for a total of 49 SC at the AA level (which is the typical legal / contractual target in the US, EU, UK).
Per wcag-conformance (opens in new window):
"Level AA: 'satisfies all the Level A and Level AA success criteria'"
"All success criteria at a claimed level must be satisfied with no exceptions."
That binary verdict ("conforms or doesn't") makes per-tool output ("axe found 17 violations in 5 pages") hard to act on - the team needs to know which SC at which level on which page is failing the conformance claim.
This skill builds that report.
When to use
If only one tool is in use, the tool's native HTML report may suffice; this skill is for aggregation across multiple tools or multiple pages.
How to use
Step 1 - Author normalizers per upstream tool
Each scanner has its own JSON shape. The first step is normalizing to a single intermediate format keyed by SC:
interface Violation {
page: string; // URL or path
successCriterion: string; // e.g. "1.4.3"
level: 'A' | 'AA' | 'AAA';
ruleId: string; // tool-specific rule (e.g. axe "color-contrast")
selector: string; // CSS selector of failing element
message: string;
helpUrl?: string; // tool's docs
scanner: 'axe' | 'pa11y' | 'lighthouse' | 'wave' | 'equal-access';
}Normalizer per tool maps tool-specific rule IDs to the SC they cover, and a central sc-mapping.json holds rule-to-SC for every tool the report consumes. The per-tool tag conventions (axe tags, pa11y codes, Lighthouse's internal map), a worked normalize_axe example, and the sc-mapping.json shape live in references/tool-normalizers.md.
Step 2 - Aggregate
def aggregate(violations):
by_sc = defaultdict(lambda: defaultdict(list)) # sc -> level -> [violation]
by_page = defaultdict(list) # page -> [violation]
pages = set()
for v in violations:
by_sc[v['successCriterion']][v['level']].append(v)
by_page[v['page']].append(v)
pages.add(v['page'])
return {
'by_sc': dict(by_sc),
'by_page': dict(by_page),
'pages': sorted(pages),
}Step 3 - Conformance verdict per level
Per wcag-conformance (opens in new window), conformance is binary per level ("All success criteria at a claimed level must be satisfied with no exceptions"). The verdict logic:
def conformance(agg, level):
"""Returns 'conformant' | 'non-conformant' | 'unknown'.
Unknown if the scan didn't cover the SCs needed to claim conformance."""
failing_scs = [sc for sc, by_level in agg['by_sc'].items()
if any(v['level'] in level_set(level) for v in
[vv for lvl, vs in by_level.items() for vv in vs])]
if failing_scs:
return 'non-conformant', failing_scs
# Did the scan actually cover all SCs needed for this level?
expected_scs = SC_BY_LEVEL[level] # ground-truth list per WCAG 2.2
covered_scs = set(agg['by_sc'].keys()) # only SCs at least one tool checks
missing = expected_scs - covered_scs
if missing:
return 'unknown', sorted(missing)
return 'conformant', []level_set('AA') returns {'A', 'AA'} because AA "satisfies all the Level A and Level AA success criteria" (wcag-conformance (opens in new window)).
The "unknown" verdict is critical - automated tools cover roughly 30% of WCAG SCs. A green automated scan ≠ AA conformance. The report must surface what wasn't scanned.
Step 4 - Per-page coverage gaps
Equally important: did every relevant page get scanned at all? A checkout flow with a missing scan on the payment page can't claim conformance per wcag-conformance (opens in new window):
"Complete Processes: When a page is part of a multi-step process, 'all web pages in the process conform at the specified level or better.'"
Author maintains a pages-to-scan.yaml:
processes:
checkout:
pages:
- /
- /products
- /cart
- /checkout
- /checkout/payment
- /checkout/confirm
account:
pages:
- /account/login
- /account/profile
- /account/ordersCompare scanned pages against the spec; flag missing ones in the report.
Step 5 - Render the report
Render the executive verdict first: a three-row table mapping each conformance level to its verdict and the reason.
| Conformance level | Verdict | Reason |
|---|---|---|
| A | ❌ non-conformant | 3 SC failing on 7 pages |
| AA | ❌ non-conformant | + 5 additional SC failing on 12 pages |
| AAA | ⚠ unknown | 14 of 28 AAA SC weren't checked by any scanner |
Below the verdict, the full report adds process-coverage, failures-by-SC, per-page-detail, and coverage-gap sections - the coverage-gap table is where the "automated tools cover ~30% of SCs, the rest is manual review" reality from Step 3 lands. The complete multi-table sample report, every section with example rows, lives in references/sample-report-format.md.
Machine output and CI
Once the markdown report renders, two operational blocks finish the pipeline: a machine-readable compliance.json for downstream dashboards / ACR gates, and the CI job that runs the scanners and aggregator on every push. Both templates live in references/machine-output-and-ci.md.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Reporting "0 axe violations" as "WCAG conformant" | Automated tools cover ~30% of SCs; the rest needs manual review. | Always include the "manual review required" section (Step 5). |
| Treating all violation severities equally | A contrast issue on a button isn't equivalent to a missing alt on a decoration. | Group by SC level (A vs AA vs AAA); within a level, count instances. |
| Aggregating across pages without page coverage spec | Missing pages don't show up; the team thinks they're covered. | Author pages-to-scan.yaml; report missing pages explicitly (Step 4). |
| Declaring conformance from a single-tool scan | Each tool has different SC coverage; a clean axe run can hide pa11y-detectable issues. | Use 2+ tools; deduplicate by (page, SC, selector). |
| Per-tool report dumps with no SC mapping | Reviewer can't tell if tool-rule "color-contrast" maps to SC 1.4.3. | Maintain sc-mapping.json (Step 1). |
| Reporting AAA verdict as a fail without context | AAA is rarely the target; failing AAA isn't a failure for an AA-targeting team. | Make the target level explicit; report the other levels as "informational". |
| Ignoring "unknown" verdict (treating uncovered SCs as covered) | False conformance claim; potential legal exposure. | Surface the unknown verdict in bold (Step 3); list the unscanned SCs. |
Limitations
References
Machine-readable output and CI integration
View source (opens in new window)Machine-readable output and CI integration
Deep reference for wcag-compliance-reporter SKILL.md. Consult when wiring the compliance report into a dashboard / ACR pipeline or running the aggregation in CI on every push.
Machine-readable output
In addition to the markdown, emit compliance.json for downstream consumption (dashboards, ASR, programmatic gates):
{
"generatedAt": "2026-05-05T14:00:00Z",
"site": "example.com",
"verdict": { "A": "non-conformant", "AA": "non-conformant", "AAA": "unknown" },
"processes": [
{ "name": "checkout", "pagesSpec": [...], "pagesScanned": [...], "complete": false },
{ "name": "account", "pagesSpec": [...], "pagesScanned": [...], "complete": true }
],
"violations": [...]
}CI integration
- name: Run scanners
run: |
npx pa11y-ci --json > pa11y.json
npx @axe-core/cli https://staging.example.com > axe.json
npx lighthouse-batch -s https://staging.example.com -o reports/
- name: Aggregate + report
run: python scripts/wcag_compliance.py \
--pa11y pa11y.json \
--axe axe.json \
--lighthouse reports/ \
--pages-spec pages-to-scan.yaml \
--out compliance/
- name: Upload
uses: actions/upload-artifact@v4
with:
name: wcag-compliance
path: compliance/WCAG 2.2 compliance report - full sample format
View source (opens in new window)WCAG 2.2 compliance report - full sample format
Deep reference for wcag-compliance-reporter SKILL.md. Consult when rendering the whole multi-table report; SKILL.md Step 5 keeps only the top verdict table and points here for the rest.
The complete report renders the executive verdict first, then process-coverage, failures-by-SC, per-page-detail, and coverage-gap sections. The full template (every section with example rows):
# WCAG 2.2 Compliance Report
**Generated:** 2026-05-05
**Site:** example.com
**Scope:** 22 pages across 4 processes
**Tools used:** axe-core 4.10.3, pa11y 8.1.0, Lighthouse 12.5.0
## Verdict
| Conformance level | Verdict | Reason |
|-------------------|---------|--------|
| A | ❌ non-conformant | 3 SC failing on 7 pages |
| AA | ❌ non-conformant | + 5 additional SC failing on 12 pages |
| AAA | ⚠ unknown | 14 of 28 AAA SC weren't checked by any scanner |
> Per [wcag-conformance][wcag-conf]: "All success criteria at a
> claimed level must be satisfied with no exceptions." Failing
> means the conformance claim cannot be made for this level.
## Process coverage
| Process | Pages spec | Pages scanned | Missing |
|-----------|-----------:|--------------:|---------------------------------|
| checkout | 6 | 5 | `/checkout/confirm` not scanned |
| account | 3 | 3 | - |
⚠ Until `/checkout/confirm` is scanned, the **checkout process**
cannot claim conformance regardless of per-page results
([wcag-conformance][wcag-conf] "Complete Processes").
## Failures by Success Criterion (Level A + AA)
| SC | Title | Level | Pages affected | Total instances |
|---------|------------------------------------|-------|---------------:|----------------:|
| 1.4.3 | Contrast (Minimum) | AA | 7 | 23 |
| 2.4.7 | Focus Visible | AA | 4 | 8 |
| 3.3.2 | Labels or Instructions | A | 3 | 5 |
| 4.1.2 | Name, Role, Value | A | 6 | 14 |
| 1.1.1 | Non-text Content | A | 2 | 4 |
(Click an SC for per-page detail.)
## Per-page detail (top failing pages)
### `/checkout/payment` - 12 violations across 5 SC
| SC | Tool | Selector | Message |
|-------|---------|-----------------------------------|---------|
| 1.4.3 | axe | `.amount-due` | Foreground/background contrast 3.2:1 (need 4.5:1 for normal text) |
| 2.4.7 | axe | `button.continue-payment` | Element does not have a visible focus indicator |
| ...
(Full per-page tables follow.)
## Coverage gaps - automated tools don't cover everything
| SC level | Total SCs | Covered by ≥1 tool | Manual review required |
|----------|----------:|-------------------:|------------------------|
| A | 25 | 15 | 10 SCs |
| AA | 24 | 17 | 7 SCs |
| AAA | 28 | 14 | 14 SCs |
**Manual review SC list (excerpt):** 1.2.1 (Audio-only / Video-only),
2.2.2 (Pause / Stop / Hide), 3.1.5 (Reading Level), ... See
`docs/manual-checklist.md` for the per-SC checklist.The verdict rows restate the binary per-level rule from wcag-conformance (opens in new window) ("All success criteria at a claimed level must be satisfied with no exceptions"): a failing level cannot claim conformance, and an unknown level means the scan never covered the SCs that level's claim needs.
Per-tool normalizers and the SC mapping
View source (opens in new window)Per-tool normalizers and the SC mapping
Deep reference for wcag-compliance-reporter SKILL.md. Consult when writing or updating a per-tool normalizer; SKILL.md Step 1 keeps only the Violation contract and the concept and points here for the implementation.
Each scanner tags violations with its own WCAG hints, so each tool needs a small normalizer that maps its native rule IDs to a WCAG Success Criterion. The mapping is curated upstream:
Worked normalizer - axe-core
# scripts/normalize_axe.py
def normalize_axe(json_blob, page_url):
out = []
for violation in json_blob.get('violations', []):
sc = sc_from_axe_tags(violation['tags']) # e.g. "1.4.3"
if not sc: continue
for node in violation['nodes']:
out.append({
'page': page_url,
'successCriterion': sc,
'level': level_from_sc(sc), # "1.4.3" → "AA"
'ruleId': violation['id'],
'selector': ' '.join(node['target']),
'message': violation['help'],
'helpUrl': violation['helpUrl'],
'scanner': 'axe',
})
return outThe central SC mapping
A single sc-mapping.json file holds rule-to-SC for every tool the report consumes. Update it whenever a tool's rule catalog changes so a renamed or newly-added rule keeps resolving to the right Success Criterion; a stale mapping silently drops violations that no longer match a known rule ID.
Related skills
a11y-violation-gate
Builds a CI gate that fails the build on **new** WCAG / a11y violations introduced by a PR while grandfathering pre-existing violations on a per-rule / per-page baseline. Aggregates verdicts from axe-core / pa11y / Lighthouse a11y / WAVE / IBM Equal Access scans. Use when a project has accumulated a11y debt and a strict "zero violations" gate would block every PR - the ratchet pattern lets the team ship while preventing regressions.
aria-authoring-patterns
Reference for the W3C ARIA Authoring Practices Guide (APG) - covers the 31 canonical interactive-widget patterns (Combobox, Dialog, Menu, Tabs, Tree, etc.), their required ARIA roles and states, the keyboard-interaction model per pattern, and the canonical-violations to watch for. Use when authoring a custom interactive widget that doesn't have a native HTML equivalent, or when reviewing one for ARIA correctness.
axe-a11y
Automated accessibility scanning across the five engines - axe-core (primary), pa11y, Lighthouse a11y, WAVE, and IBM Equal Access. Authors and runs axe-core scans via the `axe.run()` JavaScript API or the @axe-core/playwright / @axe-core/cli wrappers, parses `violations[]` into per-rule severity, configures rule disable / disable-by-tag patterns, and emits CI-gateable output; references/ carry the pa11y CLI (htmlcs + axe runners), Lighthouse CI `categories:accessibility` assertions, the WAVE API / overlay, and IBM Equal Access (Section 508) with their verified CLI / API / config. Use for any automated a11y scanner setup - axe-core for JS/TS UI test suites on every PR, and the references for CLI-only, Lighthouse-pipeline, WebAIM-branded, or Section 508 scanning.
screen-reader-test-author
Builds the full manual-accessibility artifact surface: step-by-step screen-reader test scripts for NVDA (Windows), JAWS (Windows), VoiceOver (macOS / iOS), or TalkBack (Android) with per-step keystroke + expected announcement; per-archetype WCAG 2.2 checklists (references/wcag-checklist.md); per-widget keystroke matrices pairing expected NVDA and VoiceOver announcements with the WCAG SC each row verifies (references/widget-matrix.md); and a guided NVDA / VoiceOver session protocol that merges script + checklist into a signed pass/fail session report. Use when authoring an accessibility-acceptance test, checklist, or widget matrix the team will run before sign-off, when scripting a manual a11y audit, OR when walking a tester through a guided screen-reader session.
wcag-color-contrast
Reference for WCAG 2.2 color-contrast conformance - covers SC 1.4.3 Contrast (Minimum, AA), 1.4.6 Contrast (Enhanced, AAA), 1.4.11 Non-text Contrast (AA), and 1.4.13 Content on Hover or Focus (AA) - with the canonical contrast ratios (4.5:1 normal text, 3:1 large text and UI components), measurement formula references, and bulk design-token checking patterns. Use when designing a color palette, reviewing a component for accessibility, or auditing existing CSS for contrast violations.
wcag-keyboard-navigation
Reference catalog for WCAG 2.2 keyboard-navigation conformance - covers SC 2.1.1 (Keyboard), 2.1.2 (No Keyboard Trap), 2.1.4 (Character Key Shortcuts), 2.4.3 (Focus Order), 2.4.7 (Focus Visible), 2.4.11/2.4.12 (Focus Not Obscured) - with conformance levels (A/AA), test scripts, and per-criterion failure patterns, plus the modal focus-trap / focus-management pattern (focus-on-open, Tab-cycle, inert, Escape-closes, restore-to-trigger, native `<dialog>`) in references/. Use when authoring or reviewing keyboard-only interaction support, or a modal / drawer / popover's focus management.