Tools
Tools: Accessibility Tooling for Agentic Coding Loops
2026-02-23
0 views
admin
Does it actually help? ## Anatomy of a violation ## Determining "Fixability" ## Gathering context ## Looping on diffs ## Opening up the browser ## Handling page fragments ## Architecture ## A note on the core engine ## What's next Coding agents are writing and modifying front-end code at scale. If your team maintains a design system or owns accessibility on a product, this changes the calculus: code that once went through a human review loop is now generated in seconds, often without any accessibility check at all. Existing tools weren't designed for this. They assume a human in the loop; run a scan, read the report, interpret the findings, figure out the fix. That workflow requires domain expertise at every step. An agent operating in a coding loop needs something different. Not a report to interpret, but structured diagnostics it can act on directly: machine-executable fix instructions, DOM context for reasoning, a fixability classification for triage, and a verification mechanism to confirm fixes landed. @accesslint/mcp is an MCP server built on @accesslint/core, a rule engine designed from the ground up for agent consumption. It exposes tools for auditing HTML (as a string, file, or URL), diffing before-and-after audits, and listing rules; for Claude Code, Cursor, Windsurf, or any MCP-compatible agent. This post walks through the design decisions behind the tool: how violations are structured, how fixability classification works, what context collection looks like per rule, and how the diff loop closes the audit-fix-verify cycle. Before getting into the design, here's the evidence. Both approaches - agent with MCP tools vs. agent alone - were benchmarked across 25 HTML test cases covering 67 fixable WCAG violations (3 runs each, Claude Opus): The MCP-assisted path uses 23% fewer output tokens per run. Without tools, the agent has to recall WCAG rules from training data, reason about which rules apply to which elements, and then fix them. The MCP replaces that open-ended reasoning with structured output: specific rule IDs, CSS selectors pointing to exact elements, and concrete fix suggestions. The agent skips straight to applying fixes. Fewer reasoning steps, fewer tokens, less time, lower cost. The largest gains are on complex cases. A test case with 6 violations across nested landmark structures completed in 25-38 seconds with MCP tooling. The agent alone timed out at 90 seconds in 2 of 3 runs. When an agent calls audit_html, each violation includes: Each field is deliberate: Fix is a structured instruction from a closed set: add-attribute, set-attribute, remove-attribute, add-element, remove-element, add-text-content, or suggest. The first six are mechanically executable. suggest is the escape hatch for violations where the fix depends on intent. About 75% of rules provide a mechanical fix; the remaining 25% use suggest. Fixability classifies the violation, not the fix. The button-name rule provides a mechanical fix type (add-text-content) that satisfies the rule. But its contextual classification signals that the agent should use the collected context to determine what text to add. More on this below. Context is collected per-rule from the DOM. Guidance provides the remediation principle behind the rule, written for direct LLM consumption. Browser hint, present on 26 of 92 rules, tells agents with browser access how to verify or improve a fix using screenshots or DevTools. Every rule carries a fixability classification that the MCP server surfaces on each violation: The interaction between fixability and the structured fix is the core design decision. Take button-name: the fix type is add-text-content, which is mechanically executable. But what text? The contextual classification is the signal to use the collected context. The violation reports Classes: icon-search. That's developer intent that never made it into the accessible name. The agent reads the class, infers the action, and adds aria-label="Search". This also maps to list_rules filtering. An agent or workflow can query rules by fixability to scope an audit pass: mechanical-only for automated batch remediation, contextual for agent-assisted passes, visual for flagging to human review. Each rule gathers the specific context its violation type needs. This is where the design diverges most from existing tools, which tend to report the element and stop. button-name reports CSS class names (btn-close, icon-search), the enclosing form's label, and the nearest heading. Class names are the key signal. They encode developer intent that never made it into the accessible name. A button with class icon-search inside a form labeled "Site search" gives the agent two independent signals pointing to aria-label="Search". img-alt checks whether the image is inside a link (and captures the href), looks for a figcaption, and captures adjacent text. If a figcaption already describes the image, alt="" avoids redundant adjacent text. If the image is a standalone link, the href helps the agent infer purpose. form-label reports the input's type, name, placeholder, and id, plus the full accessible name computation chain: aria-labelledby resolution, aria-label, associated <label>, title, and placeholder fallback. link-name captures the href, nearby heading text, and parent element context. For a link wrapping only an icon, the href and surrounding headings are often enough to infer purpose. The goal is to front-load enough information that the agent can reason about the fix in a single pass, without a round-trip to read more of the document. The diff loop is the verification mechanism. The workflow: The server audits the new HTML, diffs against the stored result, and returns: Violations are matched by ruleId + selector. The agent gets a clear signal: what was fixed, what regressed (with the diagnosis and fix instruction for self-correction), and what remains. The NEW category is critical. An agent that adds role="buton" (a typo) gets the regression surfaced immediately, with a structured fix to correct it. The loop is: audit, fix, diff, self-correct. No human in the middle. 26 rules carry a browser hint: an instruction for agents with browser access (screenshots, DevTools MCP) on how to verify or improve a fix. For a visual rule like color-contrast: Violation context includes computed colors and ratio. After changing colors, use JavaScript to read getComputedStyle() on the element and recalculate the contrast ratio. Screenshot the element to verify the fix looks correct in context. For a contextual rule like button-name: Screenshot the button to identify its icon or visual label, then add a matching aria-label. These are opt-in. An agent without browser tools ignores them. An agent with browser MCP tools (Chrome DevTools, Playwright) can use them to bridge the gap between static analysis and rendered output, particularly for the 4 visual rules where static analysis alone can't fully verify the fix. When the input HTML lacks <!DOCTYPE html> or <html>, the server auto-enables component mode, suppressing 22 page-level rules (document title, landmarks, lang attribute, etc.) that would produce false positives on isolated markup. The agent can override this with the component_mode parameter. This means an agent auditing a React component, a partial template, or a code snippet gets relevant results without noise. The MCP server is a thin integration layer. The rule engine is @accesslint/core: 92 rules, zero runtime dependencies, synchronous execution. The API is runAudit(doc: Document): AuditResult. The server handles HTML parsing (happy-dom), fragment detection, audit state for diffing, and violation enrichment (joining each violation with its rule's fixability, browser hint, and guidance). The core library covers 23 WCAG 2.1 success criteria across Level A and AA, scoped to what static DOM analysis can meaningfully check. Rules that throw during execution are caught and skipped; the audit always completes. The library also exports lower-level primitives (getAccessibleName, getComputedRole, isAriaHidden) and a declarative rule engine for authoring rules as JSON with validateDeclarativeRule and compileDeclarativeRule. An agent can write, validate, and register new rules at runtime. Every MCP tool call is a round-trip through the protocol, and an agent running audit-fix-diff in a loop may make dozens of them. Latency per call matters. Engines like axe-core were designed at the outset for the browser; the execution model is async and demonstrably slow1. The latency accrues. The rule engine is @accesslint/core: 92 rules, 23 WCAG 2.1 success criteria (Level A and AA), zero runtime dependencies, synchronous execution. The MCP server parses HTML with happy-dom and calls runAudit(doc): AuditResult. A typical component audit completes in single-digit milliseconds. At that speed, the bottleneck is the LLM, not the tooling. The structured output described throughout this post (fix suggestions, fixability classifications, per-rule context, browser hints) are first-class fields on every violation, not bolted on after the fact. The rules are authored with agent consumption in mind. To add the MCP server to Claude Code: Or add it to any MCP client configuration: Testing in the DOM is important for many accessibility tests, and doesn't have to be slow. Keep an eye out for improvements to the AccessLint StoryBook Addon soon that will help. I'd love to hear from you! Please share questions and comments, or drop me a note, I'm always happy to nerd out on accessibility. @accesslint/core vs axe-core benchmarks ↩ Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK:
1. [CRITICAL] labels-and-names/button-name Button has no discernible text. Element: button.icon-search HTML: <button class="icon-search" onclick="openSearch()"><svg aria-hidden="true">...</svg></button> Fix: add-text-content Fixability: contextual Browser hint: Screenshot the button to identify its icon or visual label, then add a matching aria-label. Context: Classes: icon-search Guidance: Screen reader users need to know what a button does. Add visible text content, aria-label, or aria-labelledby. For icon buttons, use aria-label describing the action (e.g., aria-label='Close'). If the button contains an image, ensure the image has alt text describing the button's action. Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
1. [CRITICAL] labels-and-names/button-name Button has no discernible text. Element: button.icon-search HTML: <button class="icon-search" onclick="openSearch()"><svg aria-hidden="true">...</svg></button> Fix: add-text-content Fixability: contextual Browser hint: Screenshot the button to identify its icon or visual label, then add a matching aria-label. Context: Classes: icon-search Guidance: Screen reader users need to know what a button does. Add visible text content, aria-label, or aria-labelledby. For icon buttons, use aria-label describing the action (e.g., aria-label='Close'). If the button contains an image, ensure the image has alt text describing the button's action. CODE_BLOCK:
1. [CRITICAL] labels-and-names/button-name Button has no discernible text. Element: button.icon-search HTML: <button class="icon-search" onclick="openSearch()"><svg aria-hidden="true">...</svg></button> Fix: add-text-content Fixability: contextual Browser hint: Screenshot the button to identify its icon or visual label, then add a matching aria-label. Context: Classes: icon-search Guidance: Screen reader users need to know what a button does. Add visible text content, aria-label, or aria-labelledby. For icon buttons, use aria-label describing the action (e.g., aria-label='Close'). If the button contains an image, ensure the image has alt text describing the button's action. CODE_BLOCK:
Summary: 2 fixed, 1 new, 3 remaining FIXED: - [CRITICAL] text-alternatives/img-alt at img[src="photo.jpg"] - [CRITICAL] labels-and-names/button-name at button.icon-search NEW: - [SERIOUS] aria/aria-roles at div[role="buton"] ARIA role "buton" is not a valid role value. Fix: set-attribute role="button" REMAINING: - [MODERATE] navigable/heading-order at h4 - [MODERATE] distinguishable/link-in-text-block at a.subtle - [MINOR] text-alternatives/image-alt-words at img[alt="image of logo"] Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
Summary: 2 fixed, 1 new, 3 remaining FIXED: - [CRITICAL] text-alternatives/img-alt at img[src="photo.jpg"] - [CRITICAL] labels-and-names/button-name at button.icon-search NEW: - [SERIOUS] aria/aria-roles at div[role="buton"] ARIA role "buton" is not a valid role value. Fix: set-attribute role="button" REMAINING: - [MODERATE] navigable/heading-order at h4 - [MODERATE] distinguishable/link-in-text-block at a.subtle - [MINOR] text-alternatives/image-alt-words at img[alt="image of logo"] CODE_BLOCK:
Summary: 2 fixed, 1 new, 3 remaining FIXED: - [CRITICAL] text-alternatives/img-alt at img[src="photo.jpg"] - [CRITICAL] labels-and-names/button-name at button.icon-search NEW: - [SERIOUS] aria/aria-roles at div[role="buton"] ARIA role "buton" is not a valid role value. Fix: set-attribute role="button" REMAINING: - [MODERATE] navigable/heading-order at h4 - [MODERATE] distinguishable/link-in-text-block at a.subtle - [MINOR] text-alternatives/image-alt-words at img[alt="image of logo"] CODE_BLOCK:
claude mcp add accesslint -- npx -y @accesslint/mcp Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
claude mcp add accesslint -- npx -y @accesslint/mcp CODE_BLOCK:
claude mcp add accesslint -- npx -y @accesslint/mcp CODE_BLOCK:
{ "mcpServers": { "accesslint": { "command": "npx", "args": ["-y", "@accesslint/mcp"] } }
} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
{ "mcpServers": { "accesslint": { "command": "npx", "args": ["-y", "@accesslint/mcp"] } }
} CODE_BLOCK:
{ "mcpServers": { "accesslint": { "command": "npx", "args": ["-y", "@accesslint/mcp"] } }
} - Mechanical (20 rules): Deterministic. A positive tabindex gets set to "0". A non-valid ARIA role gets flagged with the correct value. No ambiguity.
- Contextual (65 rules): Requires surrounding context, but an LLM can reason about it. The violation's context and guidance fields provide the inputs. The structured fix provides a safe floor.
- Visual (4 rules): Requires rendered output. Color contrast, primarily. Browser hints tell the agent how to inspect computed styles or screenshot the element. - The agent calls audit_html with name: "before" to audit and store the result.
- The agent applies fixes.
- The agent calls diff_html with the updated markup and before: "before". - @accesslint/core vs axe-core benchmarks ↩
how-totutorialguidedev.toaimlllmserverjavascriptsslgit