<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Cameron Cundiff</title>
    <description>The latest articles on Forem by Cameron Cundiff (@cameron-accesslint).</description>
    <link>https://forem.com/cameron-accesslint</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3785515%2F40bf0dd0-1ae1-465f-b595-ef3f73fdb47d.jpg</url>
      <title>Forem: Cameron Cundiff</title>
      <link>https://forem.com/cameron-accesslint</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/cameron-accesslint"/>
    <language>en</language>
    <item>
      <title>Introducing @accesslint/jest: progressive accessibility testing for Jest</title>
      <dc:creator>Cameron Cundiff</dc:creator>
      <pubDate>Sat, 18 Apr 2026 18:18:18 +0000</pubDate>
      <link>https://forem.com/cameron-accesslint/introducing-accesslintjest-progressive-accessibility-testing-for-jest-3i7j</link>
      <guid>https://forem.com/cameron-accesslint/introducing-accesslintjest-progressive-accessibility-testing-for-jest-3i7j</guid>
      <description>&lt;p&gt;This post is for the team rolling out accessibility testing: developer tooling, CI platform, design systems, frontend tooling, or their engineering managers. Enabling accessibility tests on greenfield code is straightforward. On an existing codebase it's harder: the suites typically contain hundreds or thousands of known violations no team has prioritized.&lt;/p&gt;

&lt;p&gt;Teams have typically tried four approaches and watched each erode. Gating on zero turns CI red across the suite. A suppressions file decays as reviewers add entries faster than they retire them. Per-test opt-in grows coverage slowly and leaves untouched components unchecked. Audit-once-and-ticket produces a backlog that drifts from reality. The common thread: none of them separate "the backlog that already exists" from "new regressions."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/@accesslint/jest" rel="noopener noreferrer"&gt;&lt;code&gt;@accesslint/jest&lt;/code&gt;&lt;/a&gt; addresses this rollout problem with two features: a snapshot-baseline workflow that gates CI on &lt;em&gt;new&lt;/em&gt; violations rather than total count, and a trend-report sidecar that produces the evidence leadership typically asks for to justify continued accessibility investment. It adds a &lt;code&gt;toBeAccessible()&lt;/code&gt; matcher on &lt;a href="https://www.npmjs.com/package/@accesslint/core" rel="noopener noreferrer"&gt;&lt;code&gt;@accesslint/core&lt;/code&gt;&lt;/a&gt;, an independent WCAG 2.2 engine validated against the &lt;a href="https://act-rules.github.io/" rel="noopener noreferrer"&gt;W3C ACT-R corpus&lt;/a&gt;. The rest of this post covers the rollout path, the reporting layer, the migration from &lt;code&gt;jest-axe&lt;/code&gt;, and where the two libraries overlap.&lt;/p&gt;

&lt;p&gt;For the migration command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @accesslint/codemod jest-axe &lt;span class="s1"&gt;'src/**/*.test.{ts,tsx}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Snapshot baselines with auto-ratchet
&lt;/h2&gt;

&lt;p&gt;Snapshot baselines address that gap directly. &lt;code&gt;toBeAccessible({ snapshot: "name" })&lt;/code&gt; locks the current violations as a baseline and fails only on new ones. When violations get fixed, the baseline shrinks automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;login form has no new a11y violations&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;LoginForm&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeAccessible&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;login-form&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the first run, the matcher creates &lt;code&gt;accessibility-snapshots/login-form.json&lt;/code&gt; and passes, capturing whatever violations exist today. Commit that file alongside your tests. On later runs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;New violations appear&lt;/strong&gt;, the assertion fails with the diff (only the new ones).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Some existing violations get fixed&lt;/strong&gt; and no new ones appear, the matcher trims them from the baseline. You commit the smaller snapshot file and move on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Force a full refresh&lt;/strong&gt; by running with &lt;code&gt;ACCESSLINT_UPDATE=1&lt;/code&gt; (for intentional scope changes).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;jest-axe&lt;/code&gt; doesn't offer this workflow directly. Adopting it on an existing codebase usually means either gating all tests on zero violations or writing per-test suppressions.&lt;/p&gt;

&lt;h3&gt;
  
  
  A rollout path
&lt;/h3&gt;

&lt;p&gt;For a platform-team rollout across several repos or teams:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pilot in one repo with a known backlog.&lt;/strong&gt; Wrap a handful of existing tests with &lt;code&gt;toBeAccessible({ snapshot: "..." })&lt;/code&gt;. Run once to generate baselines, commit them with the tests. Verify the workflow end to end: CI stays green on the first push, a regression PR fails cleanly, a fix PR produces a baseline diff reviewers can read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publish the matcher as a shared preset.&lt;/strong&gt; Add &lt;code&gt;@accesslint/jest&lt;/code&gt; to the &lt;code&gt;setupFilesAfterEnv&lt;/code&gt; of your org's internal Jest preset (or the shared test-config package your teams consume). Teams pick it up by upgrading the preset, not by hand-editing each repo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let teams opt in at their pace, with the preset on by default.&lt;/strong&gt; Each repo that adopts the preset generates its own &lt;code&gt;accessibility-snapshots/&lt;/code&gt; directory on first run. CI passes. The platform team isn't responsible for each repo's a11y debt; the matcher reports rather than enforcing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track the trendline automatically.&lt;/strong&gt; The matcher writes a history sidecar next to each snapshot, and &lt;a href="https://github.com/AccessLint/accesslint/tree/main/report" rel="noopener noreferrer"&gt;&lt;code&gt;@accesslint/report&lt;/code&gt;&lt;/a&gt; aggregates those into trend data for leadership. See the next section for details.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Raise the gate per test, not per project.&lt;/strong&gt; When a baseline reaches zero for a given test, the owning team drops the &lt;code&gt;snapshot&lt;/code&gt; option. That test becomes a strict gate. The transition from "managed backlog" to "strict gate" is incremental and self-directed by the owning team, and you as the platform team don't have to orchestrate a flag day.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The practical effect from the platform side: adoption doesn't block on a cleanup sprint, and rollback is cheap (remove from the preset). The cost is the baseline JSON files committed per repo, which stay inside the repos that own them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evidence that the initiative is working
&lt;/h2&gt;

&lt;p&gt;Accessibility initiatives tend to die between budget cycles. Leadership approves the work in Q1, developers fix violations through Q2, and by Q3 someone asks "where did the number go?" Without a quantitative trend, the answer is usually a screenshot of last quarter's number. Which works for one cycle.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@accesslint/jest&lt;/code&gt; emits a &lt;code&gt;.history.ndjson&lt;/code&gt; sidecar next to every snapshot. Each line records a snapshot event (created, ratchet-down, or force-update):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"ts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2026-04-18T10:42:03Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"login-form"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"ratchet-down"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"added"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"removed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"addedRules"&lt;/span&gt;&lt;span class="p"&gt;:[],&lt;/span&gt;&lt;span class="nl"&gt;"removedRules"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="s2"&gt;"text-alternatives/img-alt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"labels-and-names/form-label"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"distinguishable/color-contrast"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sidecar is append-only, committed to git alongside the snapshot JSON, and written automatically. Test authors don't need to be aware of it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/AccessLint/accesslint/tree/main/report" rel="noopener noreferrer"&gt;&lt;code&gt;@accesslint/report&lt;/code&gt;&lt;/a&gt; reads the sidecars and produces a trend at whatever scope you point it to: a single repo, a directory of repos, or the whole monorepo. Output can be rolled into whatever cadence fits: a weekly digest for the platform channel, a JSON feed for an existing reporting system, a quarterly summary for a leadership review.&lt;/p&gt;

&lt;p&gt;The property that matters: the measurement is a byproduct of the matcher running. Product teams don't instrument anything. Platform teams don't operate a service. The trend data exists because the test suite ran.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx9axfenbrbda6gullukg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx9axfenbrbda6gullukg.png" alt="AccessLint report dashboard showing burn down of issues over time." width="800" height="857"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Actionable failure messages
&lt;/h2&gt;

&lt;p&gt;When an assertion fails, the matcher writes a failure block that's designed to be useful in your test-runner output without opening a browser tab:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Expected element to have no accessibility violations, but found 2:

  [critical] text-alternatives/img-alt (WCAG 1.1.1, A) — Image element missing alt attribute.
    selector: div &amp;gt; img
    fix: add-attribute alt=""
    guidance: Every image needs an alt attribute. For informative images, describe the content or function concisely. For decorative images (backgrounds, spacers, purely visual flourishes), use alt='' to hide them from screen readers. [...]

  [critical] labels-and-names/form-label (WCAG 4.1.2, A) — Form element has no accessible label.
    selector: div &amp;gt; input
    context: type: email
    fix: Add a &amp;lt;label&amp;gt; element associated via the for attribute, or add an aria-label attribute
    guidance: Every form input needs an accessible label so users understand what information to enter. [...]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each violation includes the impact in brackets, the namespaced rule ID, the WCAG success criterion and level, a one-line fix suggestion, and prose guidance, all pulled from rule metadata in &lt;code&gt;@accesslint/core&lt;/code&gt;. The goal is that a developer reading a failing test log can usually tell what to change without leaving the terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Component-scoped auditing, by default
&lt;/h2&gt;

&lt;p&gt;When the element you assert on isn't &lt;code&gt;document.documentElement&lt;/code&gt;, page-level rules (&lt;code&gt;html-has-lang&lt;/code&gt;, &lt;code&gt;document-title&lt;/code&gt;, &lt;code&gt;region&lt;/code&gt;, &lt;code&gt;bypass&lt;/code&gt;, and others) are skipped automatically. Most Testing Library tests render into a container, so the matcher applies the right rule set for component tests without extra configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SubmitButton is accessible&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SubmitButton&lt;/span&gt; &lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Send&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// region / document-title / html-has-lang rules skip automatically&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeAccessible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For full-page audits, pass &lt;code&gt;document.documentElement&lt;/code&gt; or use &lt;code&gt;{ componentMode: false }&lt;/code&gt; to force them back on. It's a small thing, but it means component tests stop flagging landmark rules that don't apply to the unit under test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smaller ergonomic additions
&lt;/h2&gt;

&lt;p&gt;Smaller conveniences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Auto-registration.&lt;/strong&gt; &lt;code&gt;setupFilesAfterEnv: ["@accesslint/jest"]&lt;/code&gt; is the whole setup. No explicit &lt;code&gt;expect.extend()&lt;/code&gt; call, no separate &lt;code&gt;/extend-expect&lt;/code&gt; entry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Synchronous matcher.&lt;/strong&gt; &lt;code&gt;expect(container).toBeAccessible()&lt;/code&gt; returns immediately: no &lt;code&gt;await&lt;/code&gt;, no intermediate &lt;code&gt;results&lt;/code&gt; variable. Reads the same as other jest-dom matchers (&lt;code&gt;toBeInTheDocument&lt;/code&gt;, &lt;code&gt;toHaveStyle&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Impact threshold.&lt;/strong&gt; &lt;code&gt;toBeAccessible({ failOn: "serious" })&lt;/code&gt; only fails on &lt;code&gt;serious&lt;/code&gt; and &lt;code&gt;critical&lt;/code&gt; violations, useful when gating CI on a policy line rather than zero-tolerance. (&lt;code&gt;jest-axe&lt;/code&gt; has an equivalent via its &lt;code&gt;impactLevels&lt;/code&gt; config; the AccessLint variant is per-call rather than per-project.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript out of the box.&lt;/strong&gt; The package augments both the modern &lt;code&gt;@jest/globals&lt;/code&gt; &lt;code&gt;expect.Matchers&lt;/code&gt; interface and the legacy global &lt;code&gt;jest.Matchers&lt;/code&gt; namespace, so &lt;code&gt;expect(el).toBeAccessible(...)&lt;/code&gt; type-checks under either setup.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Migrating from &lt;code&gt;jest-axe&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The codemod handles the common patterns. Run with &lt;code&gt;--dry --print&lt;/code&gt; first to inspect the diff:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @accesslint/codemod jest-axe &lt;span class="s1"&gt;'src/**/*.test.{ts,tsx}'&lt;/span&gt; &lt;span class="nt"&gt;--dry&lt;/span&gt; &lt;span class="nt"&gt;--print&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A typical test file transforms to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- import { axe, toHaveNoViolations } from "jest-axe";
- expect.extend(toHaveNoViolations);
&lt;/span&gt;&lt;span class="gi"&gt;+ import "@accesslint/jest";
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;  test("form is accessible", async () =&amp;gt; {
    const { container } = render(&amp;lt;Form /&amp;gt;);
&lt;span class="gd"&gt;-   const results = await axe(container);
-   expect(results).toHaveNoViolations();
&lt;/span&gt;&lt;span class="gi"&gt;+   expect(container).toBeAccessible();
&lt;/span&gt;  });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What the codemod leaves for human review:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;configureAxe({ rules: ... })&lt;/code&gt; globals.&lt;/strong&gt; AccessLint passes options per call rather than per project. The codemod leaves &lt;code&gt;configureAxe&lt;/code&gt; imports in place and adds a &lt;code&gt;TODO(accesslint-codemod):&lt;/code&gt; comment reminding you to reapply those settings via &lt;code&gt;toBeAccessible({ disabledRules, failOn, ... })&lt;/code&gt; where needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-call rule filters with axe IDs.&lt;/strong&gt; &lt;code&gt;axe(container, { rules: { "color-contrast": { enabled: false } } })&lt;/code&gt; collapses to &lt;code&gt;expect(container).toBeAccessible()&lt;/code&gt; with a TODO. AccessLint uses namespaced IDs. Remap to &lt;code&gt;toBeAccessible({ disabledRules: ["distinguishable/color-contrast"] })&lt;/code&gt;. The &lt;a href="https://github.com/AccessLint/accesslint/tree/main/jest#migrating-from-jest-axe" rel="noopener noreferrer"&gt;jest package README&lt;/a&gt; includes a mapping table for the most common rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swapping the devDep.&lt;/strong&gt; The codemod doesn't touch &lt;code&gt;package.json&lt;/code&gt;. Run &lt;code&gt;npm install --save-dev @accesslint/jest &amp;amp;&amp;amp; npm uninstall jest-axe&lt;/code&gt; after you're happy with the diff.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full transform rules and flags are documented in the &lt;a href="https://github.com/AccessLint/accesslint/tree/main/codemod" rel="noopener noreferrer"&gt;&lt;code&gt;@accesslint/codemod&lt;/code&gt; README&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What doesn't change
&lt;/h2&gt;

&lt;p&gt;Plenty stays the same across the two libraries, which is the point:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WCAG 2.2 Level A and AA coverage&lt;/strong&gt; is the goal both implementations aim at. AccessLint and axe-core are independent rule engines; expect rule outputs to agree on most violations and differ in count or phrasing on others.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing Library compatibility is identical.&lt;/strong&gt; &lt;code&gt;toBeAccessible()&lt;/code&gt; accepts any &lt;code&gt;Element&lt;/code&gt;: &lt;code&gt;container&lt;/code&gt;, &lt;code&gt;screen.getByRole('form')&lt;/code&gt;, &lt;code&gt;document.body&lt;/code&gt;, or a plain &lt;code&gt;document.createElement(...)&lt;/code&gt;. React, Vue, Svelte, and Angular Testing Library all work the same way they did before.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jest setup shape is the same.&lt;/strong&gt; &lt;code&gt;testEnvironment: "jsdom"&lt;/code&gt; and a &lt;code&gt;setupFilesAfterEnv&lt;/code&gt; entry, one line added to your config.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Violation counts can differ from what &lt;code&gt;axe-core&lt;/code&gt; reports on the same markup. That's expected from independent implementations of the same specifications, not a bug. Treat those differences as a point to investigate rather than a regression signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install and first test
&lt;/h2&gt;

&lt;p&gt;For a new project, or to try the matcher alongside an existing test file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; @accesslint/jest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// jest.config.js&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;testEnvironment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jsdom&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;setupFilesAfterEnv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@accesslint/jest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// LoginForm.test.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@testing-library/react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;LoginForm&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./LoginForm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;LoginForm is accessible&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;LoginForm&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeAccessible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://github.com/AccessLint/accesslint/tree/main/jest" rel="noopener noreferrer"&gt;package README&lt;/a&gt; has deeper coverage of snapshot baselines, options, and framework-specific recipes for Vue / Svelte / Angular.&lt;/p&gt;

&lt;h2&gt;
  
  
  Status and roadmap
&lt;/h2&gt;

&lt;p&gt;A few things a platform team evaluating adoption should know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Versioning.&lt;/strong&gt; &lt;code&gt;@accesslint/jest&lt;/code&gt; is on semver. The 0.x series is still settling, so minor bumps may change behavior in documented ways; patch releases are reserved for bug fixes. Published with npm provenance so attestations are verifiable in your registry. License is MIT.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rule-engine stability.&lt;/strong&gt; The rule catalog ships in &lt;code&gt;@accesslint/core&lt;/code&gt; and is pinned by &lt;code&gt;@accesslint/jest&lt;/code&gt; as a workspace dependency. Rule additions arrive in core minor versions; the matcher surfaces them only on upgrade. New rules that fire on existing content appear as new violations the snapshot baselines don't yet account for, so they're treated as regressions until the owning team fixes them or force-refreshes the baseline with &lt;code&gt;ACCESSLINT_UPDATE=1&lt;/code&gt;. Plan core upgrades accordingly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sibling packages.&lt;/strong&gt; Equivalent matchers ship for Vitest (&lt;a href="https://www.npmjs.com/package/@accesslint/vitest" rel="noopener noreferrer"&gt;&lt;code&gt;@accesslint/vitest&lt;/code&gt;&lt;/a&gt;) and Playwright (&lt;a href="https://www.npmjs.com/package/@accesslint/playwright" rel="noopener noreferrer"&gt;&lt;code&gt;@accesslint/playwright&lt;/code&gt;&lt;/a&gt;). All three share a runner-agnostic matcher core, so the option surface and failure-message format are consistent across stacks. Useful if your org runs mixed runners.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reporting.&lt;/strong&gt; &lt;a href="https://github.com/AccessLint/accesslint/tree/main/report" rel="noopener noreferrer"&gt;&lt;code&gt;@accesslint/report&lt;/code&gt;&lt;/a&gt; ships alongside the matcher packages; see the Evidence section above for how it fits into the rollout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing Library ecosystem listing.&lt;/strong&gt; A page is pending review at &lt;a href="https://github.com/testing-library/testing-library-docs/pull/1535" rel="noopener noreferrer"&gt;testing-library-docs PR #1535&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feedback channel.&lt;/strong&gt; &lt;a href="https://github.com/AccessLint/accesslint/issues" rel="noopener noreferrer"&gt;GitHub Issues&lt;/a&gt; is the primary signal mechanism. Rule-coverage gaps, migration patterns the codemod doesn't cover, and framework integrations that aren't documented are all useful inputs for the v0.x series.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If &lt;code&gt;@accesslint/jest&lt;/code&gt; ends up in your org's Jest setup, please report anything you run into.&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>testing</category>
      <category>javascript</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Accessibility Tooling for Agentic Coding Loops</title>
      <dc:creator>Cameron Cundiff</dc:creator>
      <pubDate>Mon, 23 Feb 2026 00:41:23 +0000</pubDate>
      <link>https://forem.com/cameron-accesslint/accessibility-tooling-for-agentic-coding-loops-5b1h</link>
      <guid>https://forem.com/cameron-accesslint/accessibility-tooling-for-agentic-coding-loops-5b1h</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/AccessLint/mcp" rel="noopener noreferrer"&gt;@accesslint/mcp&lt;/a&gt; is an MCP server built on &lt;a href="https://github.com/AccessLint/core" rel="noopener noreferrer"&gt;@accesslint/core&lt;/a&gt;, 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does it actually help?
&lt;/h2&gt;

&lt;p&gt;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):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;With @accesslint/mcp&lt;/th&gt;
&lt;th&gt;Agent alone&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Violations fixed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;99.5% (200/201)&lt;/td&gt;
&lt;td&gt;93.5% (188/201)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Regressions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.7 / run&lt;/td&gt;
&lt;td&gt;2.0 / run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$0.56 / run&lt;/td&gt;
&lt;td&gt;$0.62 / run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Duration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;270s / run&lt;/td&gt;
&lt;td&gt;377s / run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Timeouts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0 / 63 tasks&lt;/td&gt;
&lt;td&gt;2 / 63 tasks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anatomy of a violation
&lt;/h2&gt;

&lt;p&gt;When an agent calls &lt;code&gt;audit_html&lt;/code&gt;, each violation includes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. [CRITICAL] labels-and-names/button-name
   Button has no discernible text.
   Element: button.icon-search
   HTML: &amp;lt;button class="icon-search" onclick="openSearch()"&amp;gt;&amp;lt;svg aria-hidden="true"&amp;gt;...&amp;lt;/svg&amp;gt;&amp;lt;/button&amp;gt;
   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.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each field is deliberate:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix&lt;/strong&gt; is a structured instruction from a closed set: &lt;code&gt;add-attribute&lt;/code&gt;, &lt;code&gt;set-attribute&lt;/code&gt;, &lt;code&gt;remove-attribute&lt;/code&gt;, &lt;code&gt;add-element&lt;/code&gt;, &lt;code&gt;remove-element&lt;/code&gt;, &lt;code&gt;add-text-content&lt;/code&gt;, or &lt;code&gt;suggest&lt;/code&gt;. The first six are mechanically executable. &lt;code&gt;suggest&lt;/code&gt; is the escape hatch for violations where the fix depends on intent. About 75% of rules provide a mechanical fix; the remaining 25% use &lt;code&gt;suggest&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fixability&lt;/strong&gt; classifies the violation, not the fix. The &lt;code&gt;button-name&lt;/code&gt; rule provides a mechanical fix type (&lt;code&gt;add-text-content&lt;/code&gt;) that satisfies the rule. But its &lt;code&gt;contextual&lt;/code&gt; classification signals that the agent should use the collected context to determine &lt;em&gt;what&lt;/em&gt; text to add. More on this below.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context&lt;/strong&gt; is collected per-rule from the DOM. &lt;strong&gt;Guidance&lt;/strong&gt; provides the remediation principle behind the rule, written for direct LLM consumption. &lt;strong&gt;Browser hint&lt;/strong&gt;, present on 26 of 92 rules, tells agents with browser access how to verify or improve a fix using screenshots or DevTools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Determining "Fixability"
&lt;/h2&gt;

&lt;p&gt;Every rule carries a fixability classification that the MCP server surfaces on each violation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mechanical&lt;/strong&gt; (20 rules): Deterministic. A positive &lt;code&gt;tabindex&lt;/code&gt; gets set to &lt;code&gt;"0"&lt;/code&gt;. A non-valid ARIA role gets flagged with the correct value. No ambiguity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contextual&lt;/strong&gt; (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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visual&lt;/strong&gt; (4 rules): Requires rendered output. Color contrast, primarily. Browser hints tell the agent how to inspect computed styles or screenshot the element.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The interaction between fixability and the structured fix is the core design decision. Take &lt;code&gt;button-name&lt;/code&gt;: the fix type is &lt;code&gt;add-text-content&lt;/code&gt;, which is mechanically executable. But what text? The &lt;code&gt;contextual&lt;/code&gt; classification is the signal to use the collected context. The violation reports &lt;code&gt;Classes: icon-search&lt;/code&gt;. That's developer intent that never made it into the accessible name. The agent reads the class, infers the action, and adds &lt;code&gt;aria-label="Search"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This also maps to &lt;code&gt;list_rules&lt;/code&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gathering context
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;button-name&lt;/code&gt;&lt;/strong&gt; reports CSS class names (&lt;code&gt;btn-close&lt;/code&gt;, &lt;code&gt;icon-search&lt;/code&gt;), 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 &lt;code&gt;icon-search&lt;/code&gt; inside a form labeled "Site search" gives the agent two independent signals pointing to &lt;code&gt;aria-label="Search"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;img-alt&lt;/code&gt;&lt;/strong&gt; checks whether the image is inside a link (and captures the &lt;code&gt;href&lt;/code&gt;), looks for a &lt;code&gt;figcaption&lt;/code&gt;, and captures adjacent text. If a figcaption already describes the image, &lt;code&gt;alt=""&lt;/code&gt; avoids redundant adjacent text. If the image is a standalone link, the &lt;code&gt;href&lt;/code&gt; helps the agent infer purpose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;form-label&lt;/code&gt;&lt;/strong&gt; reports the input's &lt;code&gt;type&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;placeholder&lt;/code&gt;, and &lt;code&gt;id&lt;/code&gt;, plus the full accessible name computation chain: &lt;code&gt;aria-labelledby&lt;/code&gt; resolution, &lt;code&gt;aria-label&lt;/code&gt;, associated &lt;code&gt;&amp;lt;label&amp;gt;&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, and &lt;code&gt;placeholder&lt;/code&gt; fallback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;link-name&lt;/code&gt;&lt;/strong&gt; captures the &lt;code&gt;href&lt;/code&gt;, nearby heading text, and parent element context. For a link wrapping only an icon, the &lt;code&gt;href&lt;/code&gt; and surrounding headings are often enough to infer purpose.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Looping on diffs
&lt;/h2&gt;

&lt;p&gt;The diff loop is the verification mechanism. The workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The agent calls &lt;code&gt;audit_html&lt;/code&gt; with &lt;code&gt;name: "before"&lt;/code&gt; to audit and store the result.&lt;/li&gt;
&lt;li&gt;The agent applies fixes.&lt;/li&gt;
&lt;li&gt;The agent calls &lt;code&gt;diff_html&lt;/code&gt; with the updated markup and &lt;code&gt;before: "before"&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The server audits the new HTML, diffs against the stored result, and returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Violations are matched by &lt;code&gt;ruleId + selector&lt;/code&gt;. The agent gets a clear signal: what was fixed, what regressed (with the diagnosis and fix instruction for self-correction), and what remains.&lt;/p&gt;

&lt;p&gt;The NEW category is critical. An agent that adds &lt;code&gt;role="buton"&lt;/code&gt; (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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Opening up the browser
&lt;/h2&gt;

&lt;p&gt;26 rules carry a browser hint: an instruction for agents with browser access (screenshots, DevTools MCP) on how to verify or improve a fix.&lt;/p&gt;

&lt;p&gt;For a visual rule like &lt;code&gt;color-contrast&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For a contextual rule like &lt;code&gt;button-name&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Screenshot the button to identify its icon or visual label, then add a matching aria-label.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling page fragments
&lt;/h2&gt;

&lt;p&gt;When the input HTML lacks &lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/code&gt; or &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt;, the server auto-enables component mode, suppressing 22 page-level rules (document title, landmarks, &lt;code&gt;lang&lt;/code&gt; attribute, etc.) that would produce false positives on isolated markup. The agent can override this with the &lt;code&gt;component_mode&lt;/code&gt; parameter.&lt;/p&gt;

&lt;p&gt;This means an agent auditing a React component, a partial template, or a code snippet gets relevant results without noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;The MCP server is a thin integration layer. The rule engine is &lt;a href="https://github.com/AccessLint/core" rel="noopener noreferrer"&gt;@accesslint/core&lt;/a&gt;: 92 rules, zero runtime dependencies, synchronous execution. The API is &lt;code&gt;runAudit(doc: Document): AuditResult&lt;/code&gt;. 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).&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The library also exports lower-level primitives (&lt;code&gt;getAccessibleName&lt;/code&gt;, &lt;code&gt;getComputedRole&lt;/code&gt;, &lt;code&gt;isAriaHidden&lt;/code&gt;) and a declarative rule engine for authoring rules as JSON with &lt;code&gt;validateDeclarativeRule&lt;/code&gt; and &lt;code&gt;compileDeclarativeRule&lt;/code&gt;. An agent can write, validate, and register new rules at runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  A note on the core engine
&lt;/h2&gt;

&lt;p&gt;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 slow&lt;sup id="fnref1"&gt;1&lt;/sup&gt;. The  latency accrues.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;runAudit(doc): AuditResult&lt;/code&gt;. A typical component audit completes in single-digit milliseconds. At that speed, the bottleneck is the LLM, not the tooling.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;p&gt;To add the MCP server to Claude Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add accesslint &lt;span class="nt"&gt;--&lt;/span&gt; npx &lt;span class="nt"&gt;-y&lt;/span&gt; @accesslint/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or add it to any MCP client configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"accesslint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@accesslint/mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Testing in the DOM is important for many accessibility tests, and doesn't &lt;em&gt;have&lt;/em&gt; to be slow. Keep an eye out for improvements to the &lt;a href="https://www.npmjs.com/package/@accesslint/storybook-addon" rel="noopener noreferrer"&gt;AccessLint StoryBook Addon&lt;/a&gt; soon that will help.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;&lt;a href="https://observablehq.com/d/e26301f8709bf07a" rel="noopener noreferrer"&gt;@accesslint/core vs axe-core benchmarks&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>webdev</category>
      <category>a11y</category>
      <category>javascript</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
