Skip to content

Accessibility

Fixing accessibility issues with AI coding agents: the full workflow

Audit the live site, give each finding to your coding agent as a scoped task, review the diff, then re-audit until the violation is no longer found.

Your UX Agent teamProduct
24 min read

Last updated

An AI coding agent can fix many accessibility failures quickly, but only inside a loop that it does not run on its own. Find the issues with checks that name the rule, the element, the measured value and the WCAG criterion.

Give the agent one finding at a time, with that evidence and a check that shows when it is done. Review the diff for the shortcuts agents can take with ARIA and focus. Then re-run the same check, on the same page, and look wider for anything the change broke. The criteria that need human judgment stay with a person.

This guide walks through each stage with real code, says what each check can and cannot establish, and shows one finding going all the way round. It works with any agent that edits your code.

For the specifics of one tool, see how the same loop works in Claude Code.

The loop in five stages

  1. Find. Input: the pages you ship. Output: findings, each with a rule, an element, a measured value and a criterion.Stage 1
  2. Hand over. Input: one finding. Output: a scoped task with a done-when check.Stage 2
  3. Fix. Input: the task and your project’s rules. Output: a diff, and the result of the agent’s own check.Stage 3
  4. Review. Input: the diff. Output: an accepted change, or a comment that sends it back.Stage 4
  5. Re-check. Input: the deployed change. Output: the finding no longer found, or still found, plus anything new.Stage 5, back to stage 2 while the finding is still found
The fix loop. Five stages in a loop: find, hand over, fix, review, re-check. Each stage’s output is the next stage’s input, and a finding that is still found goes back to the hand-over stage.

The agent is only stage 3. Most of what goes wrong with AI accessibility fixes happens in the stages around it: a vague finding produces a vague fix, an unreviewed diff ships a shortcut, and a fix nobody re-checks is a guess.

Stage 1: find the issues, and know what each finding proves

A finding is only as useful as the evidence behind it. Before you hand anything to an agent, know which layer of testing produced it, because each layer establishes different things.

  • Automated rules, such as axe-core’s, decide failures from the code and the rendered page: a form field with no accessible name, an image with no text alternative, text whose computed contrast is below the threshold. A rule’s failure is evidence. A rule’s silence is not a pass: it means no rule found a failure in the state the scan reached.
  • Model review can point at things a rule cannot decide, such as a text alternative that does not seem to match its image. It is judgment, and it can be plausible and wrong. Treat it as a lead for a person, not as a task for an agent.
  • Manual testing by someone who knows WCAG can cover every criterion: reading order, focus order, whether a label describes its field, whether an error message helps.
  • Assistive technology testing shows what a screen reader actually announces and whether voice control can reach each control.

Earlier layers help too. A JSX accessibility linter catches some problems in the editor, and an axe-core check inside your component or end-to-end tests catches them before merge. Both run on your source and test fixtures. Only a check on the deployed page sees your real content, real styles and the templates you actually ship. Our guide to accessibility checks during development sets up each of these layers, with config for each.

For a criterion-by-criterion view, we keep a table of which automated rules exist for each WCAG 2.2 criterion, and what a person still checks for each.

Can automated tools detect this? Only part of it. Playwright’s own accessibility-testing guide puts it this way: automated tests “can detect some common accessibility problems such as missing or invalid properties. But many accessibility problems can only be discovered through manual testing.” Hand an agent the failures a rule has established. Keep the rest for a person, or hand it over only with a person’s finding attached.

Stage 2: hand over one finding with its evidence

“Fix the accessibility issues on the checkout page” gets you a large diff that is hard to review and harder to check. A finding with evidence gets you a small one. Every task you give an agent should carry:

  • The rule and the criterion: for example axe-core’s label rule, which axe-core classifies under WCAG 4.1.2 Name, Role, Value.
  • Where: the page, the screen size, and the element, as a selector.
  • The evidence: the measured value, such as a contrast ratio of 3.45:1 against a required 4.5:1, or “no accessible name”.
  • Done when: the check that must pass, and how to run it.
  • Do not change: copy, layout and behaviour outside the finding, and any file the fix should not need.

A prompt that carries all of that looks like this:

Prompt: one missing form label, scoped, with a done-when check, Markdown
Fix one accessibility finding. Change as little as possible.​Finding: axe-core rule `label` (WCAG 4.1.2 Name, Role, Value, Level A)Page: /newsletter at 390px and 1280px wideElement: form.newsletter input[type="email"]Evidence: the input has no accessible name. The visible text "Email address"next to it is not associated with it.​Done when:- the input has a visible <label> associated with it (for/id or wrapping)- `npm run test:a11y -- -g /newsletter` reports no `label` violation- the label text is still "Email address"​Do not:- use placeholder or aria-label as the only label- change copy, layout or other fields- touch files outside src/components/NewsletterForm*

One finding per task keeps each diff small enough to review quickly. Several findings can share a task when they share a cause, such as one component that fails on every page, as long as the done-when check covers each of them.

Stage 3: let the agent work inside constraints

Standing instructions work best when they can be checked. “Make it accessible” cannot be checked. “Every input has a visible, programmatically associated label” can. Put a short block of testable rules in the file your agent reads at the start of every session: CLAUDE.md for Claude Code, AGENTS.md for agents that read it, or your tool’s custom-instruction file. GitHub’s guide to accessibility instructions for Copilot gives similar advice: concise rules in a list, in precise words such as MUST and SHOULD.

Rules file: testable accessibility rules for CLAUDE.md, AGENTS.md or custom instructions, Markdown
## Accessibility rules​- Use native elements first: <button> for actions, <a href> for navigation,  <label> for form fields, <dialog> for modals. Add ARIA only when no native  element does the job.- Every form field has a visible label associated with it. A placeholder is  not a label.- An aria-label on a control with visible text starts with that visible text.- Never remove a focus outline without replacing it with a visible  :focus-visible style.- Never put aria-hidden="true" on an element that is focusable or contains  focusable elements.- Fix contrast with the existing design tokens. Do not add new colours or  change a token's value without asking.- Do not change visible copy, alt text written by editors, or layout unless  the task says so.- Before you say a task is done, run `npm run test:a11y` and report the result.

Three more constraints make the diff reviewable:

  • Scope the files. Name the component, or the directory, the fix belongs in. An agent that cannot find the cause in that scope should say so, not patch around it somewhere else.
  • Reuse what exists. A fix that introduces a new colour, a new spacing value or a one-off component solves one finding and creates a design problem. Give the agent a design manual your agent can follow, so it knows what exists.
  • Make it run the check. An agent that runs the done-when check before it hands back catches its own failed attempts. It does not replace your re-check on the deployed page: its check runs in a dev build, on fixtures, usually at one screen size.

We go through this setup in detail for one agent, with the configuration files, in the guide to the loop set up in Claude Code with CLAUDE.md, a check command and a project skill. In Cursor, the same kind of rules go in a project rule: see how to fix accessibility issues with Cursor.

Stage 4: review the diff for the shortcuts agents take

An agent asked to clear a failure may make the smallest change that clears it, and for accessibility the smallest change is often wrong. Research on AI-assisted coding points at related weak spots: what nobody asks for, what nobody checks, and ARIA. In the CodeA11y study, a formative study of 16 developers without accessibility training using GitHub Copilot, participants never asked for accessibility, left the assistant’s stand-in values (such as placeholder alt text) unreplaced, and could not verify the result (Mowar et al., CHI 2025). In a 2025 preprint comparing GPT-4o and Qwen2.5-Coder with human-written code, Suh et al. found that the models often produced more accessible code on basics such as colour contrast and alternative text, but struggled with ARIA attributes. We cover these studies, and five gaps we reproduced, in where AI-generated accessibility gaps come from.

ARIA is where most of the risk sits. The “Read Me First” page of W3C’s ARIA Authoring Practices Guide opens with “No ARIA is better than Bad ARIA”, and warns that “Incorrect ARIA misrepresents visual experiences, with potentially devastating effects on their corresponding non-visual experiences.” The 2026 WebAIM Million found that home pages with ARIA had more detected errors on average (59.1) than pages without (42), while noting that this “does not necessarily mean that ARIA introduced these errors”, since those pages were also more complex. Read every ARIA attribute in an agent’s diff as a question: is there a native element that would do this?

These are the shortcuts to look for.

A name that only satisfies the rule

axe-core’s label rule accepts several ways of naming a field, including a non-empty placeholder, a title or an aria-label. All of them clear the rule. Only some of them help people. The HTML standard says plainly that “the placeholder attribute should not be used as an alternative to a label”: it disappears as soon as someone types.

Clears axe-core’s label rule, and is still a problem, HTML
<span class="field-label">Email address</span><input type="email" name="email" aria-label="Your email">

Here the field’s name, “Your email”, does not contain the visible text “Email address”, so a voice control user who says the visible words may not reach it (2.5.3 Label in Name), and the text is not tied to the field, so clicking it does nothing. An aria-label alone also leaves no visible label when there is no text beside the field.

Fixed: one visible label, programmatically associated, HTML
<label class="field-label" for="newsletter-email">Email address</label><input type="email" id="newsletter-email" name="email" autocomplete="email">

The fixed version gives sighted users, screen reader users and voice control users the same name, and clicking the label focuses the field.

Can automated tools detect this? axe-core’s label rule catches the field with no name at all. It cannot tell a placeholder-only or aria-label-only name from a visible, associated label, and its label-content-name-mismatch rule does not check text fields. That takes a person reading the diff or the page.

A role without the behaviour

Asked to make a clickable div accessible, an agent may add role="button" and stop. The role changes what a screen reader announces. It does not make the element focusable or respond to Enter and Space. As the APG puts it, “ARIA roles do not cause browsers to provide keyboard behaviors or styling.”

Announced as a button, but unreachable by keyboard, HTML
<div class="add-to-cart" role="button" onclick="addToCart()">Add to cart</div>
Fixed: a native button gets focus, Enter and Space for free, HTML
<button type="button" class="add-to-cart" onclick="addToCart()">Add to cart</button>

If the div was styled as a button, move those styles to the button and reset its default border and background. Do not accept a diff that adds tabindex="0" and a keydown handler to the div when a button would do.

Can automated tools detect this? Not with axe-core. A default axe-core run has no rule that tells you whether a click handler can be reached and operated with the keyboard. Tab to the control and press Enter and Space.

A name that hides the visible label

Agents like descriptive names. Given a button that says “Buy now”, one may add aria-label="Add this product to your shopping cart". The accessible name no longer contains the visible text, which fails WCAG 2.5.3 Label in Name (Level A). W3C’s reason is practical: “People who operate with voice interaction use the visible labels in their commands.” A voice control user who says “click Buy now” may get no response.

Fails 2.5.3: the name does not contain “Buy now”, HTML
<button aria-label="Add this product to your shopping cart">Buy now</button>
Fixed: the visible text is the name, HTML
<button>Buy now</button>​<!-- Or, if the button needs more context, keep the name and add a description --><button aria-describedby="product-title">Buy now</button>

W3C’s Understanding document calls putting the visible text at the start of the name a best practice. Most of the time, the right fix is to delete the aria-label.

Can automated tools detect this? axe-core has a rule for it, label-content-name-mismatch, on by default since axe-core 4.14.0. A tool on an older axe-core runs it only if the tool turns it on, and a tool that selects only axe-core’s WCAG 2.0 tags leaves it out, so check, or read the diff.

Focus styles removed or hidden

Asked to match a design that “has no blue outline”, an agent may write outline: none. Keyboard users then cannot see where they are. That fails WCAG 2.4.7 Focus Visible (Level AA), and W3C lists styling an outline away as a named failure, F78, unless something else shows focus.

Fails 2.4.7: no visible focus anywhere, CSS
*:focus {  outline: none;}
Fixed: a clear ring wherever the browser shows keyboard focus, CSS
:focus-visible {  outline: 3px solid var(--color-focus);  outline-offset: 2px;}

Use an existing focus token if your design system has one, and check that the ring has enough contrast against the backgrounds it appears on.

Can automated tools detect this? No axe-core rule checks whether focus is visible. It takes a keyboard pass, or a check that tabs through the rendered page and compares each element’s focused and unfocused styles.

Content hidden from screen reader users

To stop a screen reader “reading an icon twice”, an agent may put aria-hidden="true" on the whole link. The link vanishes for screen reader users but stays in the tab order, so keyboard focus lands on something screen readers may announce as nothing, or inconsistently.

The link is focusable but hidden from assistive technology, HTML
<a href="/cart" aria-hidden="true">  <svg class="icon" viewBox="0 0 24 24">…</svg> Cart</a>
Fixed: hide only the decorative icon, HTML
<a href="/cart">  <svg class="icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">…</svg> Cart</a>

Can automated tools detect this? Yes. axe-core’s aria-hidden-focus rule fails an aria-hidden element that is focusable or contains focusable elements.

A contrast fix that changes the brand

Asked to fix low-contrast text, an agent may darken a colour token. If that token is your brand colour, the change ripples through every page that uses it, including places that already passed. Contrast failures often have more than one acceptable fix: a darker text colour, a lighter background, a larger or bolder size. Which one is a design decision. Let the agent propose; have a person choose. Where your system already has a darker variant for text, that token is usually the answer; our guide to auditing the live site against your design system shows how to find where pages have drifted from it.

Stage 5: re-run the check, then look wider

When the diff looks right, deploy it to a preview or staging environment and re-run the check that found the problem: the same rule, on the same page, at the same screen sizes. A fix that passes only in the agent’s dev build has not been checked where your users are.

Then look wider. A change to a shared component affects every page that uses it, so re-check those templates too. Tab through the changed control. Where a person found the original problem, a person checks the fix.

There is a reason to insist on this step every time. In a 2026 paper on language-model accessibility repair (an arXiv preprint accepted at an ACM ICMI 2026 companion venue), Wanscher et al. let small open-weight models write CSS fixes for live pages without checking the result: across 100 trials, they recorded 24 improvements and 20 regressions. The setting was narrow, with models of 7 to 14 billion parameters injecting CSS through a browser extension rather than a coding agent editing source, so treat it as a direction, not a rate. Their remedy matches this guide’s re-check step: a loop that accepts a change only when the measured violations go down.

Be precise about what a re-check shows. If the rule no longer finds the failure, the failure is no longer found on what was tested. It does not prove the fix is right for every user. The same honesty applies to re-testing after fixes in a full audit.

Worked example: one missing label, from finding to re-check

This is an illustrative example on a made-up site.

1. The finding

  • Rule: axe-core label, which axe-core classifies under WCAG 4.1.2 Name, Role, Value (Level A)
  • Page: /newsletter, at 390px and 1280px wide
  • Element: form.newsletter input[type="email"]
  • Evidence: the input has no accessible name

The markup behind it:

Worked example: the newsletter form as found, HTML
<form class="newsletter">  <span class="field-label">Email address</span>  <input type="email" name="email">  <button type="submit">Subscribe</button></form>

2. The task

The prompt from stage 2, but without the “do not use placeholder or aria-label as the only label” line, and with no rule about placeholders in the project’s rules file yet.

3. The agent’s first attempt

Suppose the agent returned this:

Worked example: the agent’s first attempt, a placeholder, HTML
<input type="email" name="email" placeholder="Email address">

Its own run of the check passed: axe-core’s label rule accepts a non-empty placeholder.

4. The review

The diff fails the task’s own done-when check: there is still no <label>, and the placeholder text disappears when someone types. The review comment sends it back: “Use a <label for> with the existing text. Remove the placeholder.”

5. The second attempt

Worked example: the second attempt, a label tied to the field, HTML
<form class="newsletter">  <label class="field-label" for="newsletter-email">Email address</label>  <input type="email" id="newsletter-email" name="email" autocomplete="email">  <button type="submit">Subscribe</button></form>

The id is specific to the form, because a generic email id could clash with another form on the same page. autocomplete="email" lets browsers fill the field, which also helps with 1.3.5 Identify Input Purpose.

6. The re-check

On the preview deployment, the label rule no longer finds a failure on /newsletter at either width. A person tabs to the field, hears or sees “Email address”, and clicks the label to confirm it focuses the field. Then the newsletter component’s other placements are checked, because it appears in the site footer too.

7. What stays open

Nothing in this loop tells you whether “Email address” is the right label for your audience, or whether the form’s error messages help. Those are for a person.

Who fixes what

Kind of fixWhoExamplesWhyHow to check
Mechanical, decided by a ruleThe agent, with a quick reviewA missing lang attribute on the page, a form field or button with no name where the visible text already existsThe fix is local and the rule re-checks itRe-run the rule on the deployed page
StructuralThe agent, with a careful reviewReplacing a clickable div with a button, fixing heading levels in a template, associating error messages with their fieldsThe agent may choose ARIA over a native element, or break stylesThe rule where one exists, plus a keyboard pass
Design decisionsThe agent proposes, a person decidesContrast fixes that touch brand colours, focus styles, target sizes in a dense layoutMore than one fix is valid, and the change ripplesA person signs off, then the rule re-runs
Content and meaningA personAlt text for editorial images, link text that makes sense out of context, captions and transcripts, error messages that say how to fix the errorOnly someone who knows the content can judge itManual review
Experience with assistive technologyA person, with the technologyWhat a screen reader announces in a custom widget, whether focus order follows the taskNo rule or model can stand in for itTesting with screen readers and keyboard only
Who fixes what, by kind of fix: who makes the change, examples, why, and how to check it.

What an agent cannot judge

An agent can write alt="Two engineers reviewing a circuit board". It cannot know whether that is what the image is for on your page. These WCAG criteria depend on meaning, intent or experience, and need a person even when an agent writes the code:

  • 1.1.1 Non-text Content (A): whether a text alternative serves the image’s purpose, and whether a decorative image should be hidden instead.
  • 1.2.2 Captions (Prerecorded) (A): whether captions are complete and in sync, including speakers and meaningful sounds.
  • 1.3.2 Meaningful Sequence (A) and 2.4.3 Focus Order (A): whether the reading and focus order make sense for the task.
  • 2.4.6 Headings and Labels (AA): whether a heading or label describes what it introduces.
  • 3.3.3 Error Suggestion (AA): whether an error message tells people how to fix the error.
  • Content from your CMS: alt text, link text and headings that editors write. A code fix does not reach them, and an agent should not rewrite them without the editor.
  • How it actually sounds: what a screen reader announces for a custom component, in the browser and screen reader pairs you support.

Use the agent to make the person’s job smaller: fix the failures a rule established, so the time a person spends goes to the criteria only a person can test. For the rest of WCAG 2.2, our checklist of every A and AA criterion, with failing and fixed code says which layer can catch each one.

Tools that already do parts of this

Several tools now connect accessibility checks to coding agents. In alphabetical order by maker:

  • Deque’s Axe MCP Server brings axe-based checks and suggested fixes into MCP-capable coding agents. Deque’s page names GitHub Copilot, Cursor, Claude Code, Windsurf and VS Code.
  • GitHub’s accessibility-scanner is a GitHub Action, in public preview, that scans pages, files issues and can assign them to Copilot to propose fixes, “with humans reviewing before merging”. Its README says it cannot guarantee fully accessible code suggestions.
  • GitHub’s guide to Copilot custom instructions for accessibility shows how to write the standing rules from stage 3 for Copilot.

Whichever you use, ask the same questions of it. Does the fix get re-checked with the same rule that found it? Is model judgment kept apart from measured failures? Does a person still review the diff? Does it say which criteria it cannot check?

Checklist

  1. Every finding you hand over names a rule or a person’s finding, a criterion, a page, a screen size, an element and the evidence.
  2. Each task has a done-when check the agent can run, and a list of what not to change.
  3. Your agent’s rules file holds short, testable accessibility rules.
  4. The agent runs the check before it hands back, and reports the result.
  5. You read every ARIA attribute in the diff and ask whether a native element would do.
  6. No outline: none without a visible :focus-visible replacement.
  7. No aria-hidden on anything focusable.
  8. Brand colours, copy and editorial alt text change only with a person’s decision.
  9. You re-run the same check on the deployed change, at the same screen sizes, and on the other pages that share the component.
  10. You record the result as “no longer found”, and keep the criteria that need judgment on a person’s list.

Frequently asked questions

Can an AI coding agent make my website accessible or compliant?

No. An agent can fix many of the failures automated rules find, which removes real barriers. Conformance needs every criterion at your level met, many criteria need a person’s judgment, and knowing how your site works with assistive technology takes testing with it. Whether a law applies to your site, and what it requires, depends on your jurisdiction.

Which coding agent is best for accessibility fixes?

The loop matters more than the agent. Any agent that can edit your code, read a rules file and run a test command can work through it. The quality of the finding you give it, and your review of the diff, decide most of the result.

Should the agent run axe-core itself?

Yes, as its done-when check, so it catches its own failed attempts. It does not replace a re-check on the deployed page, which has your real content, styles and screen sizes.

Can I give the agent every finding at once?

You can, but you will get a diff you cannot review. Group findings only when they share a cause, such as one component used on every page, and keep each task small enough to review quickly.