
Company News
Socket Joins New OpenJS Program to Fund Node.js Security Work
Socket is joining the OpenJS Security Stewardship Program to fund Node.js vulnerability research, maintainer remediation, and security releases.
a11y-render-gate
Advanced tools
An accessibility gate inside the agent loop. Renders the UI an agent just wrote and reports contrast, focus visibility, hit targets and label wiring as computed facts with fix-shaped payloads.
An accessibility gate inside the agent loop.
An agent writing UI has no perception. It emits text-gray-400 on white, a
<div onClick>, an 18px icon button, and a CSS reset containing outline: none
with no replacement. Then it says "done." It cannot see the result, so it cannot
know. The human reviewing it usually can't either — nobody eyeballs a contrast
ratio.
a11y-render-gate renders the UI the agent just wrote, measures it, and hands back
computed facts with fixes attached — then blocks the turn until they're fixed.
a11y-render-gate FAIL http://localhost:5173/checkout
desktop 1440x900, light · 47 elements · 1.2s
critical focus-visible ×4
button "Continue" button.btn-primary
0 pixels change when focused — no indicator renders
→ outline is set to none with no replacement — add a :focus-visible ring.
:focus-visible {
outline: 2px solid #1a5fd0;
outline-offset: 2px;
}
same fix applies to: input#email, input#card, button "Back"
serious contrast
p "Secondary text" p.muted
#8a8a8a on #ffffff = 3.45:1, need 4.5 (14px)
→ Set color to #767676 (4.54:1 against #ffffff).
color: #767676;
| Tool | What it does | Why it doesn't close this loop |
|---|---|---|
eslint-plugin-jsx-a11y | Static analysis | Can't know --color-muted became 3.1:1 after a token change |
| axe-core | Rule engine on a live DOM | Returns rule names, not values. Gives up on gradients. Ships no focus-visibility check at all. |
| Lighthouse | Page score | Wrong granularity, slow, a report for a human |
| CI a11y jobs | Post-hoc | Runs after the agent stopped, for a different person, at 10× the fix cost |
All of them are built so a human reads a report afterwards. None make the agent check itself and fix it in the same turn.
Two things make this different:
1. Findings are shaped like fixes.
axe: color-contrast: Elements must have sufficient color contrast (serious)
<button class="btn-secondary">Continue</button>
a11y-render-gate: #8a8a8a on #ffffff = 3.45:1, need 4.5:1 (14px, normal)
→ Set color to #767676 (4.54:1 against #ffffff)
The second one an agent acts on in one step. The first requires a guess.
2. Focus visibility is measured, not inferred.
Screenshot the element at rest → focus it → screenshot again → diff the crops. Zero changed pixels means the focus indicator does not exist on screen. This isn't statically expressible, axe doesn't attempt it, and it's epidemic in generated UI because every CSS reset kills the default outline.
npm install -D a11y-render-gate
npx playwright install chromium # ~150MB, one time
npx a11y-render-gate doctor # check everything is wired up
Requires Node 20+. Chromium is the only browser used.
Bundles the MCP server, the blocking Stop hook, and the skill in one unit:
claude --plugin-dir /path/to/a11y-render-gate
The plugin runs dist/, so it needs a build first — npm install in the
a11y-render-gate directory is enough.
a11y-render-gate check http://localhost:5173/checkout # a running app
a11y-render-gate check ./component.html # a standalone file
a11y-render-gate check --story ui-button--secondary # a Storybook story
a11y-render-gate check --component src/ui/Button.tsx # one component, in isolation
a11y-render-gate check # every route in your config
a11y-render-gate check / --within '.checkout-form' # just one subtree
a11y-render-gate doctor # what can it currently reach?
Exit codes: 0 pass, 1 findings at or above your threshold, 2 couldn't check
(so CI can tell "broken" apart from "inaccessible").
| Tool | Purpose |
|---|---|
a11y_check | Render and report. Call before saying UI work is done. |
a11y_explain | Full detail for one finding, without re-running. |
a11y_status | Is the dev server up? What's configured? |
a11y_accept_baseline | Accept pre-existing debt (ask the user first). |
State behind an interaction is reachable:
a11y_check({ url: "/cart", actions: [{ click: ".open-cart" }, { wait: 300 }] })
No dev server, no Storybook needed — but it does not try to reconstruct your
build. It loads your Vite, from your node_modules, pointed at your config,
so aliases, plugins, PostCSS and Tailwind all apply exactly as they do in the app.
a11y-render-gate check --component src/ui/Button.tsx --export Button
a11y-render-gate check --component src/ui/Card.tsx --props '{"title":"Hello"}'
React, Vue, Svelte and plain modules are supported; the framework comes from your
package.json.
Components that need a router, theme or store need a wrapper, because providers cannot be inferred from source and guessing produces confident nonsense:
// a11y.wrapper.tsx
export default function Wrapper({ children }) {
return <ThemeProvider><Router>{children}</Router></ThemeProvider>;
}
a11y-render-gate check --component src/ui/Nav.tsx --wrapper ./a11y.wrapper.tsx
A component that throws or renders nothing is reported as an error, never as a pass. An empty page has no accessibility defects, so silently certifying a broken component as accessible would be the worst thing this tool could do.
Requires vite in the project. Next.js has no stable programmatic dev server —
use url or storybook there.
| Rule | What's measured | Criteria |
|---|---|---|
| contrast | Real composited ratio — alpha, ancestor backgrounds, and gradients sampled from rendered pixels | 1.4.3 / 1.4.6 |
| focus-visible | Pixel diff on focus; indicator contrast against adjacent colours | 2.4.7, 1.4.11 |
| target-size | Hit-target box, with the spacing and inline exceptions applied | 2.5.8 |
| label-wiring | Accessible name as the browser computed it, via the a11y tree | 4.1.2, 1.1.1, 2.5.3 |
| keyboard-reach | Click handlers Tab can't reach, focus traps, tab order | 2.1.1, 2.1.2, 2.4.3 |
| state-contrast | Contrast under :hover / :focus (opt-in) | 1.4.3 |
| axe | 90 structural rules as a breadth backstop (opt-in) | various |
<label for="emial"> names nothing.
aria-labelledby pointing at a removed node names nothing. Both read as
correct in review. Reading the browser's accessibility tree is the only way to
see it.:hover colour is chosen by eye and never
measured, so controls go unreadable exactly while you point at them.a11y-render-gate.config.json. Its presence is the opt-in — the Stop hook does
nothing in a project without one, so installing the plugin never changes how an
unrelated repo behaves.
{
"level": "AA",
"sources": {
"baseUrl": "http://localhost:5173",
"storybookUrl": "http://localhost:6006",
"routes": ["/", "/checkout"],
"component": { "wrapper": "./a11y.wrapper.tsx" }
},
"viewports": [
{ "name": "mobile", "width": 390, "height": 844 },
{ "name": "desktop", "width": 1440, "height": 900 }
],
"themes": ["light", "dark"],
"rules": {
"state-contrast": { "enabled": true },
"axe": { "enabled": true }
},
"ignore": [".third-party-widget"],
"failOn": ["critical", "serious"],
"baseline": ".a11y-render-gate/baseline.json"
}
Some production sites reject Playwright's default headless user-agent. Set
"userAgent" (or pass --user-agent) when a real page returns 403.
Checking dark is worth the extra pass: contrast defects hide there constantly,
because the light palette is the one anybody looks at.
The first run on a real app produces a lot. Draw a line and hold it:
a11y-render-gate check /
a11y-render-gate baseline accept --note "pre-existing, tracked in PROJ-482"
Only new findings fail from now on. Without this step the gate blocks everything on day one and gets disabled permanently, which is the most common way tools like this die.
Five guards run before it is ever willing to block:
1. stop_hook_active → exit 0 never loops
2. A11Y_RENDER_GATE_DISABLE → exit 0 escape hatch
3. no config in the project → exit 0 opt-in only
4. no UI files changed → exit 0 git-aware
5. dev server unreachable → exit 0 never block on infrastructure
the user didn't start
→ otherwise: check, and exit 2 with the findings if it fails
That conservatism is deliberate. A gate that blocks for a bad reason once gets uninstalled, and then it prevents nothing at all.
One collection pass, pure probes. A single page.evaluate captures every
element's geometry, computed styles, background stack, and ancestry. Probes are
pure functions over that data — testable with no browser, and one DOM traversal
per check instead of one per rule.
Token budget is a design constraint. A raw axe dump is 5–10k tokens; a check
that expensive gets called once and then avoided, which defeats the point, since
the value is in re-running after a fix. Target is ~1200 tokens: severity ordering,
identical fixes collapsed, capped per rule, full JSON written to
.a11y-render-gate/last-run.json as the escape valve. Re-runs report movement
(2 fixed · 1 still failing · 0 new) rather than restating everything.
False positives are worse than misses. For a gate, a wrong finding costs more
than a missed one, because it burns the tool's credibility. The WCAG 2.2 spacing
and inline exceptions are implemented; disabled-control contrast is exempt from
1.4.3 and reported as advice, never as a failure; a box-shadow focus ring is
recognised as valid. The test suite's most important case is a correct page that
must produce zero findings.
Working and covered by tests: the CLI, the MCP server, the blocking Stop hook,
the Claude Code plugin, all seven rules, and all four sources — url, html,
storybook and component. 72 tests pass, fixture-driven.
Validated against real pages, not only fixtures: a 3,000-element Wikipedia
article and a production homepage. That exercise removed three false-positive
classes that every hand-written fixture had missed — cursor: pointer alone is
no longer treated as evidence of a control, the WCAG inline exception now
measures against the containing block, and visually hidden skip links are no
longer reported for contrast.
Not built yet:
file:line.Interfaces may change before 1.0.
Not a full WCAG audit, and no substitute for testing with real assistive technology. It checks what a browser can compute. It does not evaluate whether alt text is good, only whether it exists; it does not cover cognitive accessibility; and screen-reader behaviour differs from the accessibility tree in ways this cannot see.
Automated checks catch roughly a third of WCAG issues. This aims to make that third free and immediate, not to pretend it is all of them.
npm install && npx playwright install chromium
npm test # fixture-driven, ~5s
npm run build
Fixtures in fixtures/broken/ declare their expected findings in a header
comment; fixtures/clean/ must yield zero.
MIT.
FAQs
An accessibility gate inside the agent loop. Renders the UI an agent just wrote and reports contrast, focus visibility, hit targets and label wiring as computed facts with fix-shaped payloads.
We found that a11y-render-gate 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.

Company News
Socket is joining the OpenJS Security Stewardship Program to fund Node.js vulnerability research, maintainer remediation, and security releases.

Security News
Two compromised GitHub Actions were re-enabled with malicious tags intact, exposing thousands of downstream repositories to Mini Shai-Hulud.

Research
/Security News
A malicious Firefox extension fetches its payload after installation to evade detection, steal Google session cookies, and automate account takeover.