◉CLAUDE LABJP
●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 44 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 44 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…
Articles/Claude Code
⟐ Claude Code/2026-04-30Intermediate

Automating Frontend Accessibility Audits with Claude Code — axe-core, Hooks, and PR Comments That Actually Get Read

A practical way to make accessibility audits actually stick in a real codebase. Combine axe-core with Claude Code skills, pre-commit hooks, and a GitHub Actions PR comment, with code you can paste in today.

claude-code132accessibilitya11yaxe-coreautomation109

We all know accessibility matters, and yet a11y checks are still the first thing to slip when a release deadline looms. Running four small projects in parallel, I have personally pushed "just one more sprint" enough times that the regressions piled up and forced an embarrassing catch-up week.

This piece walks through a concrete way to keep the audit in the loop without relying on willpower. We use axe-core as the runtime, and Claude Code as the connective tissue that turns raw output into something a developer actually sees and acts on.

Why "we installed an audit tool" usually fails

Most teams already have a linter, a test runner, and probably a Lighthouse step somewhere. The reason a11y still slides is not that the tooling is missing. It is that the results live in the wrong place.

A warning buried in a CI log gets ignored. A Slack notification gets scrolled past. The only spot a developer reliably reads is the diff view of the pull request they just opened. Once I started piping audit results into PR comments instead of CI logs, the same warnings that used to be ignored for weeks were fixed within hours.

Claude Code earns its keep here in two ways. First, it converts noisy axe-core output into a focused summary that says what to fix and why. Second, hooks and skills let the same check run during local commits, so the issue is caught before the PR even exists.

Step 1: Run axe-core headlessly with Playwright

Start with the smallest possible runtime. Playwright + @axe-core/playwright is the most stable combination I have used.

npm install -D @axe-core/playwright playwright @playwright/test
npx playwright install chromium

Now write one small script with a single responsibility — visit a URL, run the audit, write the result to disk. Keep it boring; clever scripts are the ones that rot.

// scripts/a11y-audit.mjs
import { chromium } from 'playwright';
import AxeBuilder from '@axe-core/playwright';
import { writeFileSync } from 'node:fs';
 
const targets = process.argv.slice(2);
if (targets.length === 0) {
  console.error('Usage: node scripts/a11y-audit.mjs <url> [<url> ...]');
  process.exit(1);
}
 
const browser = await chromium.launch();
const page = await browser.newPage();
const results = [];
 
for (const url of targets) {
  await page.goto(url, { waitUntil: 'networkidle' });
  const r = await new AxeBuilder({ page })
    .withTags(['wcag2a', 'wcag2aa']) // start at AA
    .analyze();
  results.push({ url, violations: r.violations });
}
 
await browser.close();
writeFileSync('a11y-report.json', JSON.stringify(results, null, 2));
console.log(`Found ${results.reduce((n, r) => n + r.violations.length, 0)} violations across ${results.length} pages.`);

The expected outputs are an a11y-report.json file in the repo root and a one-line summary on stdout. Both local runs and CI use exactly the same command, which keeps the operating model dead simple.

Step 2: Wrap the script as a Claude Code skill

A Claude Code skill is a tiny manual the model can invoke autonomously. Full mechanics are covered in the custom skills development guide; here is the minimum we need.

# .claude/skills/a11y-audit/SKILL.md
---
name: a11y-audit
description: "Run axe-core against a URL and summarize violations by impact. Triggers: accessibility, a11y, audit"
---
 
# How to use
 
1. Make sure `npm run dev` is running.
2. Run `node scripts/a11y-audit.mjs http://localhost:3000`.
3. Read `a11y-report.json` and bucket violations into critical / serious / moderate / minor.
4. For each critical and serious item, return the affected selector, the WCAG criterion, and a one-line fix suggestion.
5. Format the output as a Markdown block that can be pasted into a PR description.

Two things matter here. First, the trigger keywords in the description — without them, Claude Code cannot decide when to invoke the skill. Second, leave the judgment to the model. Don't try to encode "what counts as critical" in the skill itself; teams disagree, and hardcoded rules age badly.

Step 3: Add a pre-commit hook so nothing slips

Local audits only help if you remember to run them, and you won't. A Claude Code hook fixes that. The full hook system is documented in the hooks automation guide; here is the relevant snippet.

// .claude/settings.json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash(git commit*)",
        "command": "node scripts/a11y-audit.mjs http://localhost:3000 || true",
        "description": "Run a11y audit before commit (does not block the commit)"
      }
    ]
  }
}

The || true is intentional. A hook that blocks commits will be disabled within a week. A hook that surfaces information without punishing the developer will quietly do its job for years. Claude Code summarizes the result in the conversation, the developer sees it, and that is enough to change behavior over time.

Step 4: Surface the result on every pull request

The last piece is a GitHub Actions job that runs the same script and posts a comment. Broader Claude Code + Actions patterns are in this workflow guide; the audit-only version is short.

# .github/workflows/a11y.yml
name: A11y Audit
on:
  pull_request:
    branches: [main]
 
jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci
      - run: npx playwright install --with-deps chromium
      - run: npm run build && npm run start &
      - run: sleep 5 && node scripts/a11y-audit.mjs http://localhost:3000
      - name: Comment summary on PR
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const data = JSON.parse(fs.readFileSync('a11y-report.json', 'utf8'));
            const counts = data.flatMap(p => p.violations).reduce((acc, v) => {
              acc[v.impact] = (acc[v.impact] || 0) + 1;
              return acc;
            }, {});
            const body = [
              '## Accessibility audit',
              `- critical: ${counts.critical || 0}`,
              `- serious: ${counts.serious || 0}`,
              `- moderate: ${counts.moderate || 0}`,
              `- minor: ${counts.minor || 0}`,
              '',
              counts.critical || counts.serious
                ? '⚠️ Critical or serious issues remain. Please address before merging.'
                : '✅ No critical or serious violations.',
            ].join('\n');
            await github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body
            });

Because the raw a11y-report.json ships as a workflow artifact, you can also feed it back into Claude Code later to prioritize a refactor sprint. That dual use — fresh signal in the PR, persisted data for planning — is what makes the setup pay off.

Three traps that will eat 30 minutes each

I tripped on all of these. None are obvious from the docs.

SPA hydration timing. Calling axe-core immediately after page.goto() on a client-rendered app produces phantom violations because the audit runs before React mounts. waitUntil: 'networkidle' solves it; add it before you debug anything else.

Playwright binary in CI. The Chromium download is roughly 200 MB. Without caching it, every CI run pays the tax. Add actions/cache@v4 keyed on the Playwright version and the second run drops by minutes.

Too much signal at once. Reporting every minor violation from day one is overwhelming, and overwhelmed developers ignore the comment entirely. Start with critical and serious only. Lower the bar after the team has cleared the top tier — the same staged approach we use for ESLint cleanup with Claude Code.

A note on what to skip in the early days

When I first set this up, I tried to be thorough. I instrumented every page in the app, ran the audit on multiple viewport widths, and asked Claude Code to summarize all four impact levels. The PR comments became 400 lines long, and the team's reaction was exactly what you'd expect — they collapsed the comment and stopped looking.

The version that actually changed behavior is much smaller. One target URL (the most common landing page), one viewport, only critical and serious violations, and a four-line summary at the top of the comment. Everything else can be added later, after the team has built the habit of opening the section.

This is the same dynamic I see with most "quality gate" tooling. The configuration that survives contact with reality is always less ambitious than the one we want to set up on day one. The audit you can sustain for six months beats the audit you abandon in three weeks.

Where Claude Code earns its place

A reasonable question is whether you need Claude Code at all. axe-core has its own reporters; GitHub Actions can post comments without any LLM in the loop. So why route the result through a model?

The honest answer is that the value shows up over time, not on day one. After two months of running this, the most useful output isn't the per-PR comment — it's being able to ask "show me all the color-contrast violations we've flagged this quarter" or "which routes have regressed since the redesign?" Claude Code reads the accumulated report artifacts, cross-references the affected selectors with the current codebase, and produces a prioritized refactor list. That kind of analysis is awkward to script and natural to ask.

If you're starting fresh, I'd suggest installing the script and the GitHub Action first, and adding the skill once you have a few weeks of reports to look back on. That way the skill is doing work that would otherwise be tedious, instead of duplicating what the JSON already shows.

Stop relying on memory; start relying on the workflow

Accessibility lives or dies on whether the feedback shows up where developers already look. Tools alone don't fix it. A small skill, a forgiving hook, and a PR comment together do.

If you want a single concrete next step, drop scripts/a11y-audit.mjs into your project and run it once against localhost:3000. The first violations you see will tell you whether you want the hook next, the Action next, or both at once. The order does not matter as much as starting today instead of next quarter.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⟐ Claude Code2026-04-30
Composing Claude Code Skills — Building Automation a Single Skill Can't Reach
A practical guide to going beyond one-skill-at-a-time and composing multiple Claude Code Skills into real workflows. Pipeline, branching, and meta-skill patterns with full implementation examples and the failure modes you should design against.
⟐ Claude Code2026-03-28
Running Claude Code × Playwright E2E Tests in Production — Pitfalls and Decision Criteria
Implementation patterns for running E2E tests with Claude Code and Playwright in production, plus how to measure whether the investment pays back. Includes the Cloudflare Workers and Pages preview deployment pitfalls, and a decision framework that indie developers can use to judge whether E2E tests are worth the investment.
⟐ Claude Code2026-03-24
How to Automate Game Development with Claude Code × unity-mcp — A Complete Workflow from Concept to Release
Learn how to combine Claude Code with unity-mcp to automate Unity game development. The hcg-workflows skill set provides an 8-phase workflow from planning to deployment.