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

  • 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

  • axe-a11y - the scanner umbrella; its references/ carry pa11y, Lighthouse a11y, WAVE, and IBM Equal Access.
  • 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

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.