
Company News
Free Business Plan Upgrades for Open Source Maintainers
Open source maintainers are under more pressure than ever. We're raising our open source program from the Team plan to the Business plan, free.
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.
[!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.mdare optimized for agent comprehension.
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
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:
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.
https://github.com/user-attachments/assets/33ff8672-7690-4581-9ceb-d95476883c54

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.
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.
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 detection — a11y-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 contract — 0 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.
Automated accessibility testing has a real, bounded scope, and a11y-loop says so in every report rather than implying otherwise:
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%.
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.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.
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.
"type": "module")@axe-core/playwright (MPL-2.0 — see THIRD-PARTY-NOTICES.md)SKILL.md + references/, no client-proprietary formatnode --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 testgraph 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
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.
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.
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 installing — claude 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
# 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.
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.
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:
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 skill | Plugin layer | |
|---|---|---|
| Install | copy skill/a11y-loop | claude plugin install a11y-loop@… |
| Works in | 40+ Agent Skills clients | Claude Code only |
| Gives you | §0 planning, §1 generation, §2 the loop, §3 honest reporting | plan-mode enforcement, /a11y-plan |
| Required? | yes | no |
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.
# 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
| Flag | Applies to | Effect |
|---|---|---|
--json | audit, contrast | machine-readable output on stdout |
--out <path> | audit | write the JSON report to a file |
--sarif <path> | audit | also write SARIF v2.1 (see Honest Coverage) |
--interact <path.mjs> | audit | export const states = { name: async (page) => {} } to drive built states |
--headed | audit | run a visible browser, for debugging |
--no-best-practice | audit | omit axe best-practice rules (never blocking either way) |
--quiet | audit | one-line summary only |
--large | contrast | large-scale text thresholds (≥24px, or ≥18.5px bold) |
--ui | contrast | non-text / UI component threshold, 3:1, SC 1.4.11 |
--fix | contrast | suggest passing colors, lighter and darker, in OKLCh |
Run a11y-loop --help for the full, current reference.
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.
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.
If this project helps you, please consider supporting its development:
For questions and help, see SUPPORT.md. For security issues, see SECURITY.md.
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.
Chan Meng
FAQs
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.
We found that a11y-loop demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

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.

Company News
Open source maintainers are under more pressure than ever. We're raising our open source program from the Team plan to the Business plan, free.

Security News
The supply chain control that delays freshly published gems now covers lockfile generation and gem vendoring in Ruby projects.

Security News
During a UK cyber test, a Mythos 5 agent used sockpuppets, social engineering, and prompt injection to try to get a maintainer to merge malware.