Skip to content
How-To ~10 min Workflow · 3 sources

Accessibility audit workflow

Each tool catches a different layer of the same problem. Use them together, not in isolation — Figma flags missing variants before any code is written, axe flags real DOM violations after build, the spec audit catches APIs that don't enforce a11y by default.

Three sources, three layers

Any single tool will miss things. Triangulating across all three keeps the gaps small enough to ship.

Three a11y audit lanes — what each catches uniquely Three overlapping circles representing axe-core, Figma component a11y audit, and the static spec audit. Their overlaps show which kinds of issues each tool catches uniquely (DOM-time, design-time, API-time) and where they reinforce each other. axe-core DOM · runtime Figma a11y audit design · pre-handoff Spec audit API · code-review label missing on render heading-order in real DOM scrollable-region focus missing variants target-size < 24×24 color-blind simulation API doesn't enforce a11y prop missing discriminated union gaps contrast on real surfaces aria-* presence state coverage accessible name One tool tells you something is wrong. Three tools tell you what to fix and why.
The three audit lanes overlap on contrast and naming, diverge on what's design-time vs runtime. Use all three.
Source Catches Misses When to run
axe-core (browser) Real DOM violations as the component renders — labels, contrast, heading order, ARIA mistakes, keyboard traps Issues that need a specific prop combination you didn't story; design-only problems (missing variants); API-shape gaps After every build · on every Storybook story · on the deployed site
Figma a11y audit Variant coverage, focus indicator quality, non-color differentiation (WCAG 1.4.1), target size (WCAG 2.5.8), color-blind simulation Anything runtime — keyboard behavior, ARIA wiring, real-content edge cases Before handoff · per component-set · before the design ↔ code parity check
Spec / impl audit APIs that let consumers ship unlabeled components by accident; specs without required a11y props Anything that depends on rendered output Code review · before stabilising a component API

The audit loop

Five steps. Each step feeds the next; the loop closes after a verification re-run. Skip Triangulate at your peril — it's the step where you decide whether what one tool flagged is a real issue or noise.

A11y audit loop — five steps Five steps arranged left-to-right with arrows: Collect, Triangulate, Prioritize, Fix smallest first, Verify. A dashed return arrow loops from Verify back to Collect to indicate the cycle. Collect all 3 sources Triangulate map to issues Prioritize WCAG impact Fix smallest one PR each Verify re-run sources re-run on next change
Triangulate before fixing — three sources rarely all flag the same wording, but the underlying issue usually is the same.

Worked example — LlmProgress

A component all three lanes touched. Figma said it was perfect; axe said it was broken; the spec told us why and where to fix.

LlmProgress drilldown across the three audit lanes Four stacked panels showing the LlmProgress audit story. Figma reports 100/100. axe reports aria-progressbar-name violations. The spec audit shows label prop is missing. After the fix, all three lanes are green. FIGMA AUDIT 100 overall score ✓ variant coverage ✓ color blind ✓ annotations "perfect" AXE-CORE 3 violations · serious aria-progressbar-name 3 progress bars on /patterns dashboard "missing label" SPEC AUDIT ! API gap LlmProgressSpec has no label prop consumers can't pass "the why" FIX all green label?: string spec + 3 impls 36 tests pass 1 prop, 1 day The Figma score wasn't wrong — it can't see the runtime DOM. axe wasn't whining — the API really did let us miss the label.
Each lane tells a different part of the same story. The fix lands in the spec; all three turn green.

How the LLM coordinates the loop

Three MCPs cover the three audit lanes. Claude Code orchestrates — runs the audits, parses the results, makes the fix, re-runs for verification, all in one chat. No manual tool-switching.

MCP-orchestrated a11y audit Claude Code at the center calls three MCP servers — Chrome (for axe-core), figma-console (for the Figma a11y audit), and the local repo via Read tool (for the spec audit). Each connects to a separate audit lane. Claude Code orchestrates the loop parses, prioritizes, fixes Chrome MCP javascript_tool · navigate injects axe-core, runs in-page figma-console MCP audit_component_accessibility scorecard per component-set Repo · Read tool libs/spec/ · libs/<fw>/lib/<component>/ DOM-time design-time API-time
One chat, three MCP servers, three audit lanes. The LLM decides which tool to call based on the question.

The three calls

What each lane looks like in practice — copy these into a Claude Code session to start your own audit loop.

Setup — launch Claude Code with Chrome attached

Lane 1 needs Claude to drive a real browser. The --chrome flag attaches a controlled Chrome instance to the chat so the agent can navigate, inject scripts, and read the DOM. One launch covers all subsequent axe runs in the session.

claude --chrome (one-time per session)
# Launch Claude Code with the Chrome MCP attached.
# This connects the running chat to a controlled Chrome instance so
# the agent can navigate, run JS in the page, and read the DOM.
claude --chrome
# Once attached, the agent can call:
# mcp__claude-in-chrome__navigate
# mcp__claude-in-chrome__javascript_tool
# mcp__claude-in-chrome__read_console_messages
# … which is what powers the axe injection in Lane 1 above.

Lane 1 — axe-core in a Storybook iframe

run axe scoped to one story
// Inject axe-core into the Storybook story iframe and run it,
// scoped to the story root so the Storybook chrome doesn't pollute results.
const iframe = document.querySelector('#storybook-preview-iframe');
const doc = iframe.contentDocument;
await new Promise((res, rej) => {
if (iframe.contentWindow.axe) return res();
const s = doc.createElement('script');
s.src = 'https://cdn.jsdelivr.net/npm/axe-core@4.10.0/axe.min.js';
s.onload = res;
doc.head.appendChild(s);
});
const result = await iframe.contentWindow.axe.run(
doc.querySelector('#storybook-root'),
{ resultTypes: ['violations'] }
);
console.log(result.violations.map(v => v.id));
// → ["aria-progressbar-name", "button-name", ...]

Lane 2 — figma-console MCP

figma_audit_component_accessibility
// figma-console MCP — one audit per component-set.
// Returns scorecard: variant coverage, focus indicator,
// non-color differentiation (WCAG 1.4.1), target size,
// annotations, color-blind simulation.
figma_audit_component_accessibility({ nodeId: "129:20" })
// → {
// overallScore: 82,
// scores: { variantCoverage: 86, colorDifferentiation: 0, ... },
// recommendations: [
// { priority: "high", area: "color",
// message: "Add non-color indicators to 2 state variants (WCAG 1.4.1)" },
// { priority: "medium", area: "states",
// message: "Consider adding a 'disabled' variant" }
// ]
// }

Lane 3 — spec / impl audit

discriminated union for icon-only
// Static spec-side audit — read libs/spec/src/index.ts plus one
// implementation per component. Look for components where the
// public API doesn't enforce a11y by default.
//
// Bad: optional aria-label on an icon-only-capable button.
// Good: discriminated union — children OR aria-label required.
export type LlmButtonProps =
Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'children' | 'aria-label'>
& LlmButtonSpec
& (
| { children: ReactNode; 'aria-label'?: string }
| { children?: undefined; 'aria-label': string }
);

From a real audit cycle (2026-04-26)

The numbers from the run that produced this page — start to ship, five phases, end-to-end. Baseline in tasks/a11y-audit-2026-04-26.md; after-snapshot in tasks/a11y-audit-2026-04-26-after.md.

28 component-sets audited
94 → 100 median Figma score
22 / 28 components now perfect (was 13)
7 → 0 axe violations on /patterns

The eight P-critical findings split cleanly across the three lanes: four were Figma-only (focus indicator contrast, target size on Checkbox / Radio / RadioGroup, missing variants on Select and Combobox, color-only differentiation on the danger Button), three were axe-only (aria-progressbar-name, button-name, scrollable-region-focusable), and one was spec-driven (icon-only Buttons could ship unlabelled). No two lanes flagged the same node — but together they painted a complete picture.

End state: every directly-touched docs page reports zero axe violations (/workshop, /tutorial, /first-component, /patterns, /a11y-workflow, /accessibility, /install, /mcp) and the Figma file's worst component-set score is 92. Three findings are deferred to a designer (state-axis convention, protanopia rebinding, the LlmTabGroup focus reading) — none block release.

When to run which

Phase Source Why now
Component design (Figma) Figma a11y audit Catch missing variants and tap-target issues before any code is written. Cheapest fix point.
Spec change (add/edit a11y prop) Spec audit + types compile Make sure the API enforces a11y by default, not by consumer discipline. Catch missing discriminated unions.
Story added (Storybook) axe on the story root Verify the rendered DOM is correct in this exact prop combination. Stories are your test surface.
PR check axe on changed components only Fast regression gate. The Figma + spec lanes are slower; reserve them for component-level changes.
Release All three, full sweep One snapshot per release captures the baseline so the next cycle can diff against it.