Testland
Browse all skills & agents

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-gate
View source

a11y-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 axe-a11y, pa11y-a11y, lighthouse-a11y, wave-a11y, and ibm-equal-access-a11y.

Sibling gates with the same architecture: data-quality-gate, visual-baseline-gate, contract-compatibility-gate, perf-budget-gate.

When to use

  • The project has a11y debt and the team wants to gate against regressions while paying down the debt over time.
  • Multiple a11y scanners run in CI; the team wants one verdict.
  • Per-rule or per-page severity tiering matters (e.g. block on serious/critical, warn on moderate).

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

  1. Initial creation: run all scanners; emit every current violation as a fingerprint; write to a11y-baseline.json.
  2. PR adds new violations: gate fails; PR author fixes OR debates whether the violation was actually pre-existing (regenerate baseline if the team agrees).
  3. PR fixes existing violation: the violation's fingerprint disappears from the next scan; the gate's "fixed since baseline" counter increments. Manually remove the fingerprint from a11y-baseline.json in the same PR - otherwise the baseline accumulates stale entries.
  4. Quarterly review: the team reviews the baseline; any entry older than N quarters becomes a follow-up ticket.

Anti-patterns

Anti-patternWhy it failsFix
Auto-update baseline on every PRRegressions silently get grandfathered.Manual baseline updates only; reviewers verify each addition is intentional.
One severity threshold for all rulescolor-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" counterTeam has no positive feedback for cleanup work.Surface the counter prominently; tie to OKRs.
Failing only on criticalserious issues (most contrast / most ARIA) become invisible.Block on critical AND serious.

References

  • All five scanner skills: axe-a11y, pa11y-a11y, lighthouse-a11y, wave-a11y, ibm-equal-access-a11y.
  • W3C WCAG 2.2 - https://www.w3.org/TR/WCAG22/
  • Sibling gate skills (same architecture): data-quality-gate, visual-baseline-gate, contract-compatibility-gate, perf-budget-gate.

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

ScannerNative output
axe-coreJSON with violations[]; rule ID, impact, nodes.
pa11yJSON with issues[]; code (WCAG SC), type.
Lighthouse a11yLHR JSON with categories.accessibility.audits.
WAVEJSON via WebAIM API; categories with errors / warnings.
IBM Equal AccessJSON 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

Authors and runs axe-core accessibility scans - the most-deployed open-source a11y engine - via the `axe.run()` JavaScript API or the @axe-core/playwright / @axe-core/cli wrappers, parses the `violations[]` results into per-rule severity (critical / serious / moderate / minor), configures rule disable / disable-by-tag patterns, and emits JUnit-shaped output for CI gating. Use when the project ships UI tests in JavaScript / TypeScript and wants automated a11y coverage on every PR.

ibm-equal-access-a11y

Authors and runs IBM Equal Access accessibility-checker scans - IBM's open-source a11y engine with WCAG 2.0 / 2.1 / 2.2 + US Section 508 rule sets, integrating with Node / Selenium / Puppeteer / Playwright / Karma / Cypress test runners. Distinguished by IBM's enterprise-tier rule coverage and Section 508 specificity. Use when the project ships to US federal / public-sector customers (Section 508 mandate) or when the team values IBM-branded a11y reporting.

lighthouse-a11y

Configures Lighthouse CI's Accessibility category for automated accessibility testing (a11y / WCAG coverage) - `categories:accessibility` audits backed by axe-core (axe) - with per-URL minimum-score assertions (fail a build when a page's score drops below a threshold) and per-audit overrides, distinct from the Performance category that `lighthouse-perf` covers. Use when the project already runs Lighthouse CI for Web Vitals and the team wants to add accessibility coverage in the same pipeline rather than spinning up a separate scanner.

pa11y-a11y

Authors and runs pa11y accessibility scans - a CLI / Node.js tool that wraps HTML CodeSniffer (htmlcs) and / or axe-core engines - with `pa11y {url}` invocation, reporter selection (cli / csv / json / html / tsv), WCAG standard selection (WCAG2A / WCAG2AA / WCAG2AAA), and rule ignoring. Use when the project needs scriptable a11y scans without a full test framework, or when a Node-stack project wants an alternative to direct axe-core use.

screen-reader-test-author

Builds a screen-reader test narrative - a step-by-step manual test script for NVDA (Windows), JAWS (Windows), VoiceOver (macOS / iOS), or TalkBack (Android) - that exercises a specific user flow through a component or page and captures the expected announcement at each step. Use when authoring an accessibility-acceptance test the team will run before sign-off, OR when scripting a manual a11y audit.

wave-a11y

Runs WebAIM WAVE accessibility scans via the WAVE API or the browser-extension UI - produces visual overlay of errors / alerts / structural elements directly on the page, plus categorized JSON output for CI use. Use when the team values manual-review-friendly visual feedback (the WAVE overlay) alongside automated CI scans, or when a regulatory audit requires WebAIM-branded reports.

wcag-checklist-builder

Builds a per-component WCAG 2.2 accessibility checklist from a component spec - covers focus management, color contrast, ARIA roles & states, keyboard interaction, error handling, and live-region announcements - emitting a markdown checklist or YAML test plan that pairs with screen-reader-test-author for manual verification and the violation gate for automated scans. Use during component-spec review or pre-implementation acceptance.

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-focus-trap

Reference for **intentional** focus management in modal / dialog / drawer / popover components - the canonical pattern that satisfies WCAG SC 2.4.3 (Focus Order) without violating SC 2.1.2 (No Keyboard Trap). Covers focus-on-open, focus-cycle-within-container, Escape-closes-and-restores, return-to-trigger, and inert-the-rest-of-the-page. Use when authoring or reviewing any component that displays content over the page (modals, drawers, popovers, command palettes).

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. Use when authoring or reviewing keyboard-only interaction support.

widget-a11y-test-matrix

Per-widget manual accessibility test matrices where every row pairs one keystroke with the expected focus behavior, the expected NVDA announcement, the expected VoiceOver announcement, and the WCAG 2.2 success criterion that row verifies. Covers button, toggle button, checkbox, text input, modal dialog, menu button, and combobox archetypes, plus universal Tab traversal. Use when a rendered widget has cleared automated scanning and a tester needs a fill-in pass/fail sheet to run by hand against NVDA and VoiceOver.