CFN Accessibility Gate
Purpose: Local WCAG gate. Renders each target URL in a headless browser, injects axe-core, runs the WCAG ruleset, and turns every violation into a suggestion in the shared cfn-vote-implement schema. Findings route through voting. This skill never auto-fixes.
This is the accessibility counterpart to cfn-security-review and cfn-dep-audit: capture findings, emit a manifest, route through cfn-vote-implement. It is NOT a CI job and not a GitHub Action. It runs locally against pages you serve.
What It Checks
axe-core evaluates rendered DOM against the configured WCAG tags. Typical violations surfaced:
| Class | Example axe rule |
|---|---|
| Color contrast | color-contrast |
| Missing labels | label, button-name, link-name |
| Missing alt text | image-alt |
| ARIA misuse | aria-*, aria-valid-attr |
| Keyboard / focus traps | focus-order-semantics, tabindex |
| Document structure | document-title, html-has-lang, landmark-* |
Each offending node becomes one suggestion (rule id is the category).
Dependency Requirement (axe-core must be preinstalled)
This skill follows the build ladder: it reuses an installed dep and never installs one for you (supply-chain cooldown + user-permission rule). The driver is Node + @axe-core/playwright + playwright. If they are absent the gate exits 3 and prints the exact install line:
npm install --save-dev @axe-core/playwright playwright && npx playwright install chromium
cfn: the gate assumes axe-core is preinstalled. Upgrade trigger: bundle a pinned local copy of axe-core under lib/ if cross-project install drift becomes a problem.
Where the deps are resolved from
The deps belong to the project being scanned, not to this skill directory. execute.sh walks up from the invocation cwd (and from the git project root, when different), collects every node_modules directory it finds, and exports them as NODE_PATH before invoking the runner. Without that, Node would resolve from the skill directory and miss correctly-installed project deps. Any pre-existing NODE_PATH is appended, not discarded.
The runner is lib/axe-runner.cjs. The .cjs extension is required, not cosmetic: the runner uses CommonJS require(), and most modern projects (and the skill repo itself) declare "type": "module", which would otherwise make Node load it as an ES module and fail with require is not defined.
Exit 3 means "not installed" and nothing else
Both the execute.sh preflight and the runner treat a failure as a missing dependency only when it is a module-resolution error (MODULE_NOT_FOUND / ERR_MODULE_NOT_FOUND) whose message names @axe-core/playwright or playwright. Every other failure (syntax error, ESM/CJS mismatch, a throw inside a dep, a broken transitive require) exits 4 and prints the original error message and stack. A user is never told to install a package that is already installed.
Inputs
Target URLs (at least one required):
CFN_A11Y_URLS(env): comma-separated list. Examplehttp://localhost:3000,http://localhost:3000/about.--url <url>(flag, repeatable): one URL per flag.--url=<url>form also accepted.
If no URL is provided, the gate exits 2 with usage. URLs are NOT hardcoded; this repo is mostly CLI/scripts and the gate is for frontend projects.
WCAG level:
CFN_A11Y_TAGS(env, defaultwcag2a,wcag2aa): comma-separated axe tags. Use e.g.wcag2a,wcag2aa,wcag21aato widen coverage.
Outputs
- Summary to stdout (URL count, tags, violation count).
- When violations exist: a manifest at
<project-root>/.cfn-cache/manifests/cfn-a11y-gate-<ns>.jsonin the shared schema (auto-gitignored, nanosecond-precision filename). - When no violations: a plain report, no manifest.
Exit Codes
| Code | Meaning |
|---|---|
| 0 | Scan ran, no violations. No manifest. |
| 1 | Violations found. Manifest emitted. |
| 2 | Usage error: no target URLs provided. |
| 3 | Missing dependency: node absent, or a genuine module-resolution failure naming @axe-core/playwright / playwright. Install line printed, plus the NODE_PATH that was searched. |
| 4 | Runtime error: browser launch, navigation, or runner failure. Also any dep-load error that is NOT a module-resolution failure. The original error message and stack are printed. |
Usage
# env-driven targets
CFN_A11Y_URLS="http://localhost:3000,http://localhost:3000/about" \
$HOME/.claude/skills/cfn-a11y-gate/execute.sh
# flag-driven targets
$HOME/.claude/skills/cfn-a11y-gate/execute.sh \
--url http://localhost:3000 --url http://localhost:3000/about
# widen WCAG level
CFN_A11Y_TAGS="wcag2a,wcag2aa,wcag21aa" \
$HOME/.claude/skills/cfn-a11y-gate/execute.sh --url http://localhost:3000
# route any findings through voting
/cfn-vote-implement latest
Serve the site first (dev server, or a static python -m http.server). The gate renders whatever the URL returns.
Manifest Schema (shared with cfn-vote-implement)
{
"review_id": "a11y-gate-<ns>",
"source": "cfn-a11y-gate",
"generated_at": "ISO-8601",
"status": "pending_review",
"wcag_tags": "wcag2a,wcag2aa",
"urls_scanned": ["http://localhost:3000"],
"suggestions": [
{
"id": "S001",
"category": "image-alt",
"tag": "block | fix | harden",
"one_liner": "http://localhost:3000 img: image-alt: Images must have alternate text",
"title": "image-alt: Images must have alternate text",
"description": "Ensures <img> elements have alternate text or a role of none/presentation",
"files": ["http://localhost:3000 :: img"],
"impact": "high",
"effort": "low",
"suggested_approach": "Element has no alt attribute. See https://dequeuniversity.com/rules/axe/4.x/image-alt",
"status": "pending",
"related_suggestions": []
}
]
}
Mapping: axe rule id -> category. axe impact -> impact (critical/serious to high, moderate to medium, minor to low) and tag (critical/serious to block, moderate to fix, minor to harden). axe help text -> description. axe failure summary + helpUrl -> suggested_approach. URL + node selector -> files locator. Suggestions sorted by impact.
Tags: block (serious/critical, treat as merge blockers), fix (clear violation), harden (minor, defense-in-depth).
Rules
- Local only. Not a GitHub Action and not CI.
- Never auto-fix. Every finding routes through
/cfn-vote-implement. - Never auto-install deps. Missing axe-core exits
3with the install line. block-tagged findings are merge blockers regardless of vote outcome.
Related
/cfn-vote-implement- votes on and routes the findings (never implement manually).cfn-security-review- the code-level security gate (same emit-manifest flow).cfn-dep-audit- the dependency-level gate (same exit-code convention).cfn-design- design-phase accessibility planning (this skill is the post-render check).