🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

a11y-loop

Package Overview
Dependencies
Maintainers
1
Versions
8
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

a11y-loop

Makes AI coding agents decide accessibility while the work is still being planned and write accessible UI by default, then proves what it can prove with a real browser audit across the states they built — and tells you exactly what it could not check.

latest
Source
npmnpm
Version
0.2.4
Version published
Maintainers
1
Created
Source

[!IMPORTANT]

🤖 Read this with your AI agent — don't read it by hand.

This repo is written agent-first. Point Claude Code, GitHub Copilot, Cursor, or any agent at it: "Read the README and AGENTS.md, then help me run / extend this." Structure + AGENTS.md are optimized for agent comprehension.

a11y-loop logo

a11y-loop

Write accessible UI by default. Verify it in a real browser. Say exactly what you couldn't check.

a11y-loop makes AI coding agents write accessible UI by default, then proves what it can prove with a real browser audit across the states it built — and tells you exactly what it could not check.

Demo GIF · Documentation · SKILL.md · Benchmark · Changelog · Report Bug · Request Feature

npm version License Contributors Forks Stars Issues Sponsor

Node.js Playwright axe--core WCAG 2.2 AA Agent Skills

📑 Table of Contents

🌟 Introduction

AI coding agents write inaccessible UI by default — 84% of AI-generated web pages carry accessibility issues (W4A'24), and telling the model to "be accessible" barely moves that number: accessibility-oriented prompts have measured a slightly higher violation rate than accessibility-agnostic ones (W4A'25, 17.32% vs. 15.93%). Instructions alone are not the fix.

a11y-loop is two things working together:

  • An Agent Skill — plan rules that apply while the work is still being scoped (conformance target, per-component criteria, the product decisions that foreclose accessibility, color tokens before components), then standing generation rules (semantic HTML first, ARIA discipline, APG keyboard contracts, labels, focus visibility, AA contrast in light and dark, reduced motion, 24×24 targets) that apply while the agent is writing UI code.
  • A Node CLI (a11y-loop audit / contrast --fix / diff) that verifies the result in a real browser, feeds failures back to the agent to fix, and re-audits until the loop converges.

The skill follows the open Agent Skills standard, so the same SKILL.md is portable across 40+ clients — Claude Code, Cursor, GitHub Copilot, Codex, Gemini CLI, and more — not just one vendor's agent. The CLI runs the same checks standalone in CI or any other pipeline.

a11y-loop is a personal open-source project. It exists because the same handful of failures — low contrast, missing alt text, empty links and buttons, unlabelled form inputs — have topped the WebAIM Million every year for a decade, and the reason is not that they are hard to fix. It is that accessibility usually arrives as an after-the-fact compliance check, once the UI is already built and nobody wants to touch it. The goal here is to move it earlier: make accessibility a default of the development workflow rather than an audit at the end — which matters more now that so much UI code is written by AI agents in the first place.

"Earlier" now reaches back past the first line of code. Some of the decisions that settle whether an interface can be made accessible — reordering that only works by dragging, a menu that only opens on hover, data that only exists as canvas pixels — are made while the work is being scoped, and they are nearly free to change there and a rewrite afterwards. §0 of the skill puts those decisions in the plan, where they are still cheap.

What this is not. a11y-loop does not claim compliance, does not guarantee accessibility, and does not replace manual testing or testing with assistive technology. A clean report means "no automatically detectable failures" — see Honest Coverage below for exactly what that does and doesn't cover.

🎥 Promo Video

https://github.com/user-attachments/assets/33ff8672-7690-4581-9ceb-d95476883c54

🎬 Demo

a11y-loop fixing a seeded-violation demo page across five audit passes until it converges on zero violations

The demo page (demo/before/) is a fictional conference site seeded with 163 detectable failures spanning the WebAIM Million's top six failure classes plus the failure modes documented for LLM-generated UI: clickable divs, hidden focus indicators, skipped heading levels, broken ARIA references, and ignored prefers-reduced-motion. Running the loop against it converges on demo/after/0 violations — and a11y-loop diff --before demo/before/report.json --after demo/after/report.json reports:

Converged: all 163 violations fixed, none introduced

FIXED / NEW / REMAINING status per finding, matched by stable fingerprint (rule + selector + WCAG success criterion), is what makes that verdict checkable rather than asserted — see demo/before/VIOLATIONS.md and demo/after/FIXES.md for the finding-by-finding record.

✨ Key Features

1 Five rendering passes per audit — default (1280×720), dark mode, forced-colors mode, reduced-motion, and a 320×256 reflow viewport (the WCAG-sanctioned 400% zoom equivalent for SC 1.4.10) — because most real failures only show up under a specific rendering condition, not on a single default-viewport load.

Diagram: the five passes feeding one report
graph LR
    P1["Default<br/>1280×720"] --> R["One JSON report<br/>per audited page/state"]
    P2["Dark mode"] --> R
    P3["Forced-colors mode"] --> R
    P4["Reduced motion"] --> R
    P5["320×256 reflow<br/>(400% zoom equiv., SC 1.4.10)"] --> R

2 Checks axe-core can't run — tab order, focus visibility (including focus-ring contrast), dialog focus trap / Escape / focus-return, target size (24×24 CSS px, SC 2.5.8), reduced-motion effectiveness, ambiguous link text, div-as-button, and positive tabindex. These sit alongside axe-core, not instead of it.

3 State coverage, not just page-load--interact drives the states an agent just built (a modal opened, a menu expanded, a form in its error state) through the same five passes, because axe running once on page load finds nothing in a dialog that only misbehaves once it's open.

4 Stable fingerprints power regression detectiona11y-loop diff matches findings across two reports by rule + selector + WCAG success criterion and classifies each as FIXED, NEW (a regression — this fails the command even if the total count went down), or REMAINING.

5 Structured, honest output — JSON is the primary format: every finding carries its WCAG success criterion, ACT rule ID where one exists, and a provenance block (axe-core version, browser, URL, viewport, timestamp, states exercised). SARIF v2.1 is available as a secondary format with a documented limitation rather than a silent gap. A generated manual-review checklist, scoped to what was actually built, ships with every run.

6 Exit codes are the loop's contract0 no violations / threshold met / no regression, 1 violations found / regression introduced, 2 tool error — so a CI pipeline or an agent's own control flow can branch on the result without parsing prose.

7 contrast --fix — checks a foreground/background pair against WCAG 2.x (1.4.3: 4.5:1 normal text / 3:1 large text; 1.4.11: 3:1 for UI components) and, on failure, suggests passing colors in both directions (lighter and darker) in OKLCh, so a fix stays close to the original hue instead of jumping to black or white.

8 Portable as an Agent Skill — plain SKILL.md + references/, no proprietary format, works in any client implementing the open Agent Skills standard, not only Claude Code.

9 Accessibility enters at the plan, not the pull request — §0 gives the agent the decisions to settle while the work is still being scoped: the conformance target for the relevant jurisdiction, per-component acceptance criteria, the color tokens (verified with contrast --fix, which needs no browser and no code), the structure, and the interaction states that will need auditing. It also names the product choices that foreclose accessibility — drag-only reordering (SC 2.5.7), hover-only menus (SC 1.4.13), canvas-rendered data (SC 1.1.1), time limits (SC 2.2.1), CAPTCHA (SC 3.3.8) — each with the alternative, while changing them is still a sentence rather than a rewrite. In Claude Code, the optional plugin layer enforces it at plan approval.

📊 Honest Coverage

Automated accessibility testing has a real, bounded scope, and a11y-loop says so in every report rather than implying otherwise:

  • Deque's own research puts automated coverage at ~57% of accessibility issues by volume across a 13,000+ page / ~300,000 issue study — but only ~31% of WCAG 2.2 AA success criteria have any automated rule at all, and only ~13% are reliably automatable end-to-end.
Diagram: WCAG 2.2 AA success criteria, by automation reach
pie showData title WCAG 2.2 AA success criteria — automation reach (Deque research)
    "Reliably automatable end-to-end (~13%)" : 13
    "Have some automated rule, not fully reliable (~18%)" : 18
    "No automated rule — needs manual/AT review (~69%)" : 69

Illustrates the same figures cited above, not a new metric — a clean report never implies coverage of the other ~87%.

  • A clean a11y-loop report means "no automatically detectable failures were found" — it is never a conformance or compliance claim, and the tool will not tell you your app is accessible, compliant, or free of legal risk. No single score is ever produced.
  • Every run emits a generated manual-review checklist, scoped to the components actually built, naming the criteria that need a human and/or assistive-technology testing (screen reader behavior, descriptive quality of alt text and link text, logical reading order, caption/media alternative accuracy, cognitive accessibility) — the direct inverse of "no manual testing needed."
  • axe-core's own incomplete results are surfaced as needsReview, not suppressed. See evals/benchmark-results.md for a live example of two such findings being investigated and resolved rather than dismissed.
  • SARIF caveat, stated honestly: GitHub Code Scanning only displays SARIF results that carry a file-path location. a11y-loop's findings are located by rendered URL + CSS selector, which Code Scanning drops on ingestion — a naive upload produces an empty Code Scanning view. SARIF output is provided for the Azure DevOps SARIF viewer, the VS Code SARIF extension, and other SARIF-consuming tooling; the JSON report remains the primary, complete format.
  • Findings are tagged with the lowest WCAG version that contains them (2.0 / 2.1 / 2.2), so you can filter to the subset a given jurisdiction actually enforces — e.g. US ADA Title II and Section 508 to the 2.0/2.1 AA subset, EU EN 301 549 to 2.1 AA (moving to 2.2 AA around October 2026), NZ and UK to the full 2.2 AA set.

📈 Benchmark

A small, illustrative comparison in evals/benchmark-results.md: the same six UI components, built by the same model (Claude Sonnet 5, as a Claude Code subagent), once with no accessibility guidance and once following the skill and running the audit loop to convergence.

  • Baseline (no guidance): 35 violations across 6 components. With the skill, 1 violation on first generation — before any audit ran.
  • After the loop: 0 violations, at an average of 1.67 completed audit iterations per component.

Read this with its stated caveats: N = 6 components, a single run per condition, one model family, and the audits are produced by a11y-loop's own engine (mitigated, not eliminated, by every finding being grounded in axe-core, a third-party rules engine) — a small, self-audited illustration of the effect's shape, not a controlled study or a precise effect size.

🛠️ Tech Stack

  • Runtime: Node.js ≥ 20, ESM ("type": "module")
  • Browser automation: Playwright (Chromium)
  • Accessibility engine: axe-core via @axe-core/playwright (MPL-2.0 — see THIRD-PARTY-NOTICES.md)
  • Color math: culori (OKLCh contrast fixes)
  • Focus order: tabbable
  • SARIF conversion: axe-sarif-converter
  • Agent integration: the open Agent Skills standard — SKILL.md + references/, no client-proprietary format
  • Tests: the built-in node --test runner, 337+ tests across unit and integration suites, including an 18-fixture seeded-violation matrix, a demo end-to-end run, and a forced-colors gradient regression test

🏗️ Architecture

System overview
graph TD
    A["Agent Skill<br/>skill/a11y-loop/SKILL.md + references/"] -->|"§0 plan rules"| P["Agent plans the UI work<br/>conformance target, per-component criteria,<br/>foreclosing decisions, color tokens, structure"]
    P -->|"contrast --fix, no browser needed"| E
    P --> B
    A -->|"§1 standing generation rules"| B["Agent writes UI code<br/>HTML / JSX / Vue / Svelte / Astro / CSS"]
    B --> C["a11y-loop CLI"]
    C --> D["audit<br/>5 passes: default, dark,<br/>forced-colors, reduced-motion, 320px reflow"]
    C --> E["contrast --fix<br/>WCAG 2.x + OKLCh suggestions"]
    C --> F["diff<br/>FIXED / NEW / REMAINING"]
    D --> G["Playwright + Chromium"]
    G --> H["axe-core"]
    G --> I["a11y-loop's own checks<br/>focus, dialog trap, target size,<br/>reduced motion, link text, div-button"]
    D -->|--interact| J["Drive built states:<br/>modal open, menu expanded, form error"]
    H --> K["JSON report<br/>WCAG SC + ACT IDs + provenance"]
    I --> K
    J --> K
    K --> L["SARIF v2.1<br/>(secondary format)"]
    K --> M["Manual-review checklist"]
    K -->|violations found| B
    F --> K
Sequence: one audit-fix-reaudit cycle
sequenceDiagram
    participant Agent
    participant CLI as a11y-loop CLI
    participant Browser as Playwright + Chromium
    participant Axe as axe-core + a11y-loop's own checks
    Agent->>CLI: a11y-loop audit (after writing UI code)
    CLI->>Browser: render page across 5 passes<br/>(+ --interact states, if any)
    Browser->>Axe: run checks against each rendered pass
    Axe-->>CLI: findings (WCAG SC, ACT ID, provenance)
    CLI-->>Agent: JSON report + manual-review checklist
    alt violations found
        Agent->>Agent: fix code
        Agent->>CLI: a11y-loop audit (re-run)
    else no violations
        Agent->>CLI: a11y-loop diff --before --after
        CLI-->>Agent: Converged — FIXED/NEW/REMAINING summary
    end

The loop, in words: the skill sets standing rules while the agent writes UI; a11y-loop audit verifies the rendered result across five passes plus any built interaction states; violations feed back to the agent to fix; a11y-loop diff confirms convergence without new regressions; the JSON report (and its manual-review checklist) is the artifact of record, with SARIF offered as a secondary format for tools that consume it.

🚀 Getting Started

Prerequisites

  • Node.js ≥ 20
  • A Chromium install for Playwright (installed in the steps below)

Point your agent at this section. Every command below is non-interactive, idempotent, and safe to re-run — no prompts, no TTY, no menus. An agent can install and verify the whole thing without asking you to click anything. The one exception is called out explicitly at the end.

Install everything (agent-runnable, one block)

npm i -g a11y-loop
npx playwright install chromium
mkdir -p ~/.claude/skills
cp -r "$(npm root -g)/a11y-loop/skill/a11y-loop" ~/.claude/skills/
claude plugin marketplace add ChanMeng666/a11y-loop
claude plugin install a11y-loop@chanmeng-a11y-loop --scope user

That is: the CLI, its browser, the portable skill, and the optional Claude Code plugin. No git clone — the skill ships inside the npm package. Verify:

a11y-loop --version
test -f ~/.claude/skills/a11y-loop/references/plan-phase.md && echo "skill ok"
claude plugin list | grep -A2 'a11y-loop@'   # expect: Status: ✔ enabled

Skip the last two lines if you only want the portable skill, or the mkdir/cp pair if you only want the plugin. Do not do both: the plugin carries its own copy, so the model ends up seeing a11y-loop and the directory-qualified a11y-loop:a11y-loop, paying the always-on token cost twice. If both names show up, delete ~/.claude/skills/a11y-loop and keep the plugin.

Upgrading is a different command from installingclaude plugin install on something already installed reports "already installed" and does nothing:

npm i -g a11y-loop@latest
cp -r "$(npm root -g)/a11y-loop/skill/a11y-loop" ~/.claude/skills/   # if using the skill
claude plugin marketplace update chanmeng-a11y-loop                  # if using the plugin
claude plugin update a11y-loop@chanmeng-a11y-loop

Installation (details)

# Run it directly, no install
npx a11y-loop <command>

# Or install the CLI globally
npm i -g a11y-loop

# Either way, install the Chromium build Playwright needs for audits
npx playwright install chromium

Prefer working from source (e.g. to contribute)? Clone and install instead:

git clone https://github.com/ChanMeng666/a11y-loop.git
cd a11y-loop
npm install
npx playwright install chromium

If you keep browser binaries off the system drive, set PLAYWRIGHT_BROWSERS_PATH before running npx playwright install chromium (and before running the test suite, which launches the same browser) — e.g. PLAYWRIGHT_BROWSERS_PATH=D:\playwright-browsers.

Installing the Agent Skill

The skill is a directory. Copy it wherever your client reads skills from — no clone required, since npm i -g a11y-loop already put it on disk:

SKILL_SRC="$(npm root -g)/a11y-loop/skill/a11y-loop"

# Personal, all projects (Claude Code and other clients that read ~/.claude/skills)
mkdir -p ~/.claude/skills && cp -r "$SKILL_SRC" ~/.claude/skills/

# Or project-scoped, committed with the repo
mkdir -p .claude/skills && cp -r "$SKILL_SRC" .claude/skills/

From a checkout, skill/a11y-loop is the same directory. Any client implementing the Agent Skills specification loads it the same way — Claude Code, Cursor, GitHub Copilot, Codex, Gemini CLI, and more. Re-running the copy upgrades in place.

Optional: the Claude Code plugin layer

The skill above is the whole product and it is portable. This repository also ships a thin Claude-Code-only layer that adds one thing the portable skill cannot do — enforcement during plan mode:

claude plugin marketplace add ChanMeng666/a11y-loop
claude plugin install a11y-loop@chanmeng-a11y-loop --scope user

Both are ordinary CLI commands — no TTY, no prompts, safe to re-run. (They also exist as /plugin marketplace add … and /plugin install … inside a session, but an agent should use the CLI form.) Managing it later is equally non-interactive: claude plugin disable a11y-loop, claude plugin enable a11y-loop, claude plugin uninstall a11y-loop@chanmeng-a11y-loop — add -y only if you pass --prune, which is the single flag that asks for confirmation.

From a local checkout, claude plugin marketplace add ./ registers the working copy instead, so the plugin tracks your edits rather than the published repo.

Team-wide, declaratively. --scope project writes .claude/settings.json, which you commit — after that a clone needs no install commands at all:

{
  "extraKnownMarketplaces": {
    "chanmeng-a11y-loop": { "source": { "source": "github", "repo": "ChanMeng666/a11y-loop" } }
  },
  "enabledPlugins": { "a11y-loop@chanmeng-a11y-loop": true }
}

The one thing an agent cannot do for you. Project-scoped plugins and hooks are gated behind Claude Code's workspace-trust prompt the first time that directory is opened. That is a deliberate security boundary in Claude Code — code from a repo should not execute because a file said so — and nothing here can or should bypass it. --scope user avoids it entirely, which is why the agent-runnable block above uses user scope.

It contributes:

  • a PreToolUse hook matched to ExitPlanMode. When a plan changes UI work and says nothing about accessibility, the plan is declined once and the ### Accessibility section is handed back to fill in. It names any decision already in the plan that is hard to walk back later — drag-only reordering, hover-only menus, infinite scroll, canvas-rendered data, time limits, CAPTCHA, autoplay.
  • /a11y-plan, to ask for that section directly.

Deliberate limits: it defers rather than allows on every non-deny path, so it never suppresses your own plan approval; it declines a given plan at most once, so it cannot loop; a plan with no UI in it is passed silently; and A11Y_LOOP_PLAN_GATE=off disables it. The gate checks that the question was asked — it cannot check that the answer is any good. That is still a11y-loop audit and a human.

Portable skillPlugin layer
Installcopy skill/a11y-loopclaude plugin install a11y-loop@…
Works in40+ Agent Skills clientsClaude Code only
Gives you§0 planning, §1 generation, §2 the loop, §3 honest reportingplan-mode enforcement, /a11y-plan
Required?yesno

⚙️ Using it in CI

a11y-loop audit http://localhost:3000 --json --out report.json

exits 1 if any violation is found (2 on a tool error such as a missing browser), so a build step can gate on it directly:

a11y-loop audit http://localhost:3000 --json --out report.json || exit 1

a11y-loop diff --before base.json --after head.json in a PR check turns that into a regression gate: it fails only on genuinely NEW violations, so a PR that fixes ten and introduces none passes even though the raw count changed.

📖 Usage Guide

Basic Usage

# Audit a running page (five passes: default, dark, forced-colors, reduced-motion, reflow)
a11y-loop audit http://localhost:3000

# Audit an HTML file (served locally, never over file://)
a11y-loop audit --file ./dist/index.html

# Audit an HTML fragment directly — the usual entry point for an agent mid-generation
a11y-loop audit --html "<button class=\"icon-btn\"><svg .../></button>"

# Audit an interactive state (e.g. a modal after it opens)
a11y-loop audit http://localhost:3000 --interact ./states/modal-open.mjs

# Check a color pair against WCAG 2.x and get OKLCh fix suggestions
a11y-loop contrast "#767676" "#ffffff" --fix

# Compare two audit reports for regressions
a11y-loop diff --before base.json --after head.json

Advanced Configuration

FlagApplies toEffect
--jsonaudit, contrastmachine-readable output on stdout
--out <path>auditwrite the JSON report to a file
--sarif <path>auditalso write SARIF v2.1 (see Honest Coverage)
--interact <path.mjs>auditexport const states = { name: async (page) => {} } to drive built states
--headedauditrun a visible browser, for debugging
--no-best-practiceauditomit axe best-practice rules (never blocking either way)
--quietauditone-line summary only
--largecontrastlarge-scale text thresholds (≥24px, or ≥18.5px bold)
--uicontrastnon-text / UI component threshold, 3:1, SC 1.4.11
--fixcontrastsuggest passing colors, lighter and darker, in OKLCh

Run a11y-loop --help for the full, current reference.

⌨️ Development

npm install
npx playwright install chromium   # once, or after a Playwright version bump

npm test                # full suite
npm run test:unit       # unit tests only
npm run test:integration  # integration tests only (drives real Chromium)

The optional plugin layer is plain Node with no dependencies. Exercise the plan gate directly:

echo '{"session_id":"s1","tool_name":"ExitPlanMode","tool_input":{"plan":"Add a settings page in React: a modal dialog, a members table with row actions, tabs, and a dark mode toggle styled with Tailwind CSS."}}' \
  | node hooks/plan-gate.mjs        # expect a "deny" verdict
claude plugin validate . --strict   # shape only — see AGENTS.md on why this is not proof

Silence and exit 0 is the gate's normal answer, so an example that prints nothing proves nothing: the plan has to clear 40 characters and be unambiguously about UI, or you are testing the pass path by accident. Re-running the same plan returns defer rather than deny — the loop guard spends each plan's hash once, deliberately — so prefix with CLAUDE_PLUGIN_DATA="$(mktemp -d)" for a clean verdict. See AGENTS.md for the rest of the plugin-layer gotchas.

See AGENTS.md for AI-agent-oriented project conventions, the fixture-manifest testing pattern, the loop discipline expected when touching demo/ or other UI, and the rule that keeps skill/ free of any Claude-Code-specific mechanism.

🤝 Contributing

Contributions make the open-source community an amazing place to learn and create. Please read the Contributing Guide and the Code of Conduct before you start, and use the provided issue / pull-request templates.

❤️ Sponsor

If this project helps you, please consider supporting its development:

Sponsor on GitHub Buy Me a Coffee

For questions and help, see SUPPORT.md. For security issues, see SECURITY.md.

📄 License

This project is released under the MIT license. axe-core, a dependency this project uses to run its checks, is separately licensed under MPL-2.0 — see THIRD-PARTY-NOTICES.md for full attribution.

🙋‍♀️ Author

Chan Meng

Email GitHub

Chan Meng

Chan Meng
Need a custom app like this one? I build them — let's talk.

Email Chan Meng Chan Meng on GitHub

Keywords

accessibility

FAQs

Package last updated on 31 Jul 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts