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.
Install with skills.sh (any agent)
npx skills add testland/qa --skill a11y-violation-gatea11y-violation-gate
Overview
A binary "all or nothing" a11y gate blocks every PR on a project with accumulated debt, so teams disable it. The ratchet pattern fixes this: the gate fails only on new violations vs. a stored baseline. Existing violations are grandfathered; fixes shrink the baseline.
This skill builds that gate, aggregating outputs from any combination of the five scanners covered by axe-a11y (axe-core, pa11y, Lighthouse a11y, WAVE, IBM Equal Access).
Sibling gates with the same architecture: data-quality-gate, visual-baseline-gate, contract-compatibility-gate, perf-budget-gate.
When to use
If the project is a11y-clean already, prefer a strict scanner- native gate (e.g. axe-core configured to fail on any violation) without ratchet - simpler.
Step 1 - Run the scanners and unify their outputs
Each scanner emits its own report shape (axe-core violations[], pa11y issues[], Lighthouse categories.accessibility.audits, WAVE categories, IBM Equal Access results[]). Normalize every finding to one unified record:
{
"scanner": "axe",
"rule_id": "color-contrast",
"wcag_sc": "1.4.3",
"page_url": "/dashboard",
"selector": "button.primary",
"severity": "serious",
"fingerprint": "axe::color-contrast::/dashboard::button.primary"
}The fingerprint is the load-bearing field - same fingerprint across runs = same violation; a new fingerprint = a new violation. Per-scanner output shapes and field mappings are in references/gate-implementation.md.
Step 2 - Maintain a baseline
The baseline is a checked-in JSON file listing every grandfathered fingerprint:
{
"version": 1,
"updated_at": "2026-05-04T12:00:00Z",
"violations": [
"axe::color-contrast::/legacy-page::div.subtitle",
"axe::label::/old-form::input#user_email",
"pa11y::WCAG2AA.Principle1.Guideline_1_4.1_4_3.G18.Fail::/legacy-page::span.muted"
]
}Check it into the repo at a11y-baseline.json. Update it deliberately (as part of cleanup PRs); never auto-update from CI.
Step 3 - Apply the gate decision
Grandfather any fingerprint in the baseline, then block on new violations by severity tier:
| Severity tier | Behavior |
|---|---|
| Block (critical/serious) | Fail the build. |
| Warn (moderate) | Surface in PR comment; no build failure. |
| Info (minor) | Log; no PR comment unless count > N. |
The runnable gate (axe-core ingestion shown; extend to the other scanners per references/gate-implementation.md):
# scripts/run_a11y_gate.py
import json, sys
from pathlib import Path
records = []
axe_path = Path("axe-results.json")
if axe_path.exists():
axe = json.loads(axe_path.read_text())
url = axe.get('url', '/')
for v in axe.get('violations', []):
for node in v.get('nodes', []):
sel = node.get('target', ['?'])[0]
records.append({
'scanner': 'axe', 'rule_id': v['id'],
'wcag_sc': v.get('tags', [None])[-1],
'page_url': url, 'selector': sel,
'severity': v.get('impact', 'moderate'),
'fingerprint': f"axe::{v['id']}::{url}::{sel}",
})
baseline = set()
bp = Path("a11y-baseline.json")
if bp.exists():
baseline = set(json.loads(bp.read_text()).get('violations', []))
seen = {r['fingerprint'] for r in records}
new = [r for r in records if r['fingerprint'] not in baseline]
blockers = [r for r in new if r['severity'] in ('critical', 'serious')]
warnings = [r for r in new if r['severity'] == 'moderate']
fixed = [f for f in baseline if f not in seen] # shrinking baseline
verdict = 'no-go' if blockers else 'go'
print(f"# A11y Gate - verdict: {verdict.upper()}")
print(f"blockers={len(blockers)} warnings={len(warnings)} fixed={len(fixed)}")
for r in blockers:
print(f"- {r['scanner']} :: {r['rule_id']} on {r['page_url']} ({r['selector']})")
sys.exit(0 if verdict == 'go' else 1)The shrinking baseline counter (fixed) is the positive signal: when baseline fingerprints disappear from the latest scan, the team fixed them. Surface it as "5 fixed / 47 remaining." A no-go verdict exits non-zero so CI halts.
Step 4 - Emit the PR artifact
For a PR comment or $GITHUB_STEP_SUMMARY, render the verdict as a markdown table grouping blockers, warnings, the grandfathered count, and the fixed-since-baseline count, then a recommended-next-step block. Full template: references/gate-implementation.md.
Step 5 - Baseline maintenance workflow
The baseline is shared state - careful coordination prevents bit rot:
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Auto-update baseline on every PR | Regressions silently get grandfathered. | Manual baseline updates only; reviewers verify each addition is intentional. |
| One severity threshold for all rules | color-contrast and bypass (skip-link) have different impact; uniform threshold over- or under-blocks. | Per-rule severity overrides; align with W3C-published rule severities. |
| Scoring "any violation = fail" | Tests every PR against the entire backlog; team disables the gate. | Ratchet against the baseline; only fail on net-new. |
| Skipping the "fixed-since-baseline" counter | Team has no positive feedback for cleanup work. | Surface the counter prominently; tie to OKRs. |
Failing only on critical | serious issues (most contrast / most ARIA) become invisible. | Block on critical AND serious. |
References
a11y-violation-gate: scanner shapes and artifact template
View source (opens in new window)a11y-violation-gate: scanner shapes and artifact template
Detail moved out of SKILL.md to keep the core gate script lean. The runnable gate (axe-core ingestion) is in SKILL.md Step 3; extend it to the other four scanners using the shapes below, then emit the PR artifact with the template at the end of this file.
Scanner native output shapes
| Scanner | Native output |
|---|---|
| axe-core | JSON with violations[]; rule ID, impact, nodes. |
| pa11y | JSON with issues[]; code (WCAG SC), type. |
| Lighthouse a11y | LHR JSON with categories.accessibility.audits. |
| WAVE | JSON via WebAIM API; categories with errors / warnings. |
| IBM Equal Access | JSON with results[]. |
Normalizing the other scanners
Each scanner is read the same way as axe-core: pull its findings array (the second column above), map its native fields onto the unified record {scanner, rule_id, wcag_sc, page_url, selector, severity}, and build the same fingerprint f"{scanner}::{rule_id}::{page_url}::{selector}". A finding whose fingerprint reappears on the next run is the same violation; a new fingerprint is a new violation.
Full PR artifact template
Render for $GITHUB_STEP_SUMMARY or a PR comment:
# A11y Gate - verdict: NO-GO
**Blockers (NEW violations): 2**
| Scanner | Rule | WCAG SC | Page | Selector | Severity |
|---------|-------------------|---------|--------------|---------------------|----------|
| axe | color-contrast | 1.4.3 | /checkout | button.primary | serious |
| axe | aria-required-attr | 4.1.2 | /checkout | div[role="dialog"] | critical |
**Warnings (NEW moderate): 1**
| Scanner | Rule | WCAG SC | Page | Selector |
|---------|---------------|---------|--------------|----------|
| pa11y | landmark-one-main | 1.3.1 | /checkout | (page-level) |
**Grandfathered (in baseline): 47**
**Fixed since baseline: 5** (positive trend)
## Recommended next step
Block-tier violations must be fixed in this PR. To address the
two blockers:
- `button.primary` on `/checkout`: contrast ratio 3.8:1; needs >=4.5:1.
- `div[role="dialog"]`: missing `aria-labelledby` or `aria-label`.A no-go verdict exits non-zero so CI halts.
Related skills
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-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.
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.