Scaling agentic A11y with browser-side scans
Fanning out agents to read every component file is a tempting shape for accessibility review. It demos well in a small project. In a real codebase with hundreds of components, multiple routes, and several active branches, it's a budget problem. You're paying LLM rates for work a deterministic audit engine does in JavaScript. The asymmetry is the point. Rule checks against a rendered DOM are deterministic JavaScript, not LLM tokens. The agent's leverage is in the judgment: choosing the right alt text, the right label, applying the edit at the right place in source. Spend tokens on revisions, not discovery. This morning I ran that loop against a deployed React app. A browser-side audit returned 29 violation types from the live DOM. The agent opened only the two source files those violations pointed to, applied fixes, and that was the work. No file walk, no codebase scan. A naive agent scan walks every component file: read, reason, report. Cost grows with the size of the repo. Run it on every PR plus a nightly sweep across long-lived branches, and you're paying for everything that exists, every cycle. A browser audit is bounded by what's actually rendered. One injection runs against the page, returns a structured violation list, and the agent only opens the files implicated by what came back. Cost scales with what's broken, not with what exists. That's the difference between "we'd love to run this nightly" and "we are running this nightly." The MCP version is concrete. With @accesslint/mcp paired with a browser MCP (chrome-devtools-mcp recommended), the agent does roughly this: Calls audit_browser_script and gets back a short JS snippet. Asks the browser MCP to evaluate_script that snippet on whatever URL is loaded. The snippet bootstraps @accesslint/core from a CDN, runs the audit, returns JSON. Calls audit_browser_collect and gets back [{ ruleId, selector, html, impact, message, ... }, ...]. For each violation, greps source for stable hooks in the snippet (data-testid, aria-label, visible text, class names) to locate the file. Applies the fix at the source location. Steps 1 to 4 are deterministic JavaScript. Steps 5 and 6 are where agent tokens get spent, and they scale with the number of violations, not the size of the codebase. The same loop runs against production URLs. You don't need a dev server, a preview deploy, or a Docker image. A URL is enough to find the issues; pair it with the standard GitHub permissions a code-writing agent already needs (contents and pull-requests write), and a scheduled background agent can open overnight PRs against the issues an a11y engine flags: alt attributes, ARIA values, contrast bumps, missing labels, structural fixes. PR review is where overreach gets caught. A minimal nightly GitHub Action: name: AccessLint nightly scan on: schedule: - cron: '0 6 * * *' workflow_dispatch: permissions: contents: write pull-requests: write jobs: audit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' - name: Install Claude Code and MCPs run: | npm i -g @anthropic-ai/claude-code npx playwright install --with-deps chromium claude mcp add accesslint -- npx -y @accesslint/mcp claude mcp add playwright -- npx -y @playwright/mcp@latest - name: Audit and apply fixes env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} run: | claude -p \ --allowedTools "mcp__accesslint__*,mcp__playwright__*,Read,Write,Edit,Glob,Grep" \ "Audit https://your-app.example.com using the accesslint and playwright MCPs. For each violation, find the source file in this repo and apply the fix. Use your judgment on content (alt text, label wording, heading copy); preserve component intent and avoid invasive refactors. Do not modify tests." - name: Open PR if there are changes env: GH_TOKEN: ${{ github.token }} run: | [ -z "$(git status --porcelain)" ] && exit 0 git config user.name "accesslint" git config user.email "[email protected]" git checkout -B a11y/nightly git add -A && git commit -m "a11y: mechanical fixes from rendered DOM audit" git push --force-with-lease origin a11y/nightly gh pr view a11y/nightly >/dev/null 2>&1 || gh pr create \ --title "a11y: mechanical fixes from nightly audit" \ --body "Automated fixes from a live audit." \ --base main --head a11y/nightly Swap in your URL and store ANTHROPIC_API_KEY as a repo secret. If nothing changed, the step exits cleanly with no PR. Subsequent runs force-push to the same branch, so reviewers see one rolling PR rather than a new one each night. The same shape works against a preview-deploy URL if you'd rather audit branch builds than production: add a pull_request trigger and point at the preview environment. When you're refactoring with the dev server up, the same logic applies. The audit runs in the browser against localhost, not against your JSX. Static analysis of source can't see computed colors, conditional ARIA state, or labels on children that haven't mounted. Pseudo-transpiling components into jsdom doesn't help. jsdom doesn't lay out, doesn't measure fonts, doesn't compute contrast. The DOM the user sees is the most honest input, regardless of where the page is hosted. The fastest path is the Claude Code plugin from the AccessLint marketplace: /plugin marketplace add AccessLint/claude-marketplace /plugin install accesslint@accesslint That installs the MCP server and the slash commands together: /accesslint:audit-and-fix , /accesslint:audit-react-component, and the live-page review prompts. Pair it with a browser MCP (chrome-devtools-mcp recommended) and you have the full loop. If you'd rather skip the marketplace, @accesslint/mcp drops into any MCP-compatible host as a standalone server (no slash commands, just the tools). Browser does the finding. Agent does the fix.
