Claude Code Accessibility Workflow: Semantic HTML, ARIA, axe, and Manual Checks
A practical Claude Code accessibility workflow covering HTML, ARIA, forms, focus, axe, and screen readers.
Accessibility is not a final “run Lighthouse and fix whatever appears” task. It starts with semantic HTML, continues through keyboard behavior and focus management, and only then becomes automated testing, screen-reader checks, and release discipline.
Claude Code can make that workflow faster, but only when the request is specific. If you prompt it with “make this accessible,” it may add ARIA to a div, miss the keyboard behavior, and produce a diff that looks responsible while still blocking real users. The safer approach is to give Claude Code a target, a scope, and the exact checks you expect.
This guide uses primary references: W3C WCAG 2.2 for the practical standard, the WAI-ARIA Authoring Practices Guide for widget patterns, MDN ARIA guidance for “semantic HTML first,” Deque axe-core documentation for automated checks, and the official Claude Code docs for Claude Code basics.
Set the Target Before Editing
For a product team, “accessible” is too vague. A usable target is “WCAG 2.2 AA as the default, semantic HTML before ARIA, keyboard operation for every core path, visible focus, readable errors, and both automated and manual checks.” That target is concrete enough for Claude Code to review and narrow enough for a pull request.
| Area | Practical baseline | Common failure |
|---|---|---|
| Semantic HTML | Use button, a, form, label, main, and nav for their real purpose | Clickable div elements replace native controls |
| Keyboard | Tab, Shift+Tab, Enter, Space, and Escape cover the main workflow | A modal can only be closed with a mouse |
| Focus | Focus enters new UI and returns to the trigger when it closes | Focus escapes behind a dialog |
| ARIA | Add it only when native HTML cannot express the state | aria-label hides missing visible labels |
| Contrast | Text, controls, errors, and focus indicators are visible | Low-contrast gray text passes design review but not use |
| Forms | Labels, hints, required state, and errors are connected to inputs | Errors appear visually but are not announced |
| Testing | Use axe plus manual keyboard and screen-reader checks | The team treats zero automated violations as complete proof |
This order matters. ARIA is a supplement, not a replacement for the platform. MDN explicitly recommends using native semantic HTML when it already provides the semantics and behavior you need. Claude Code should be guided to improve the HTML first, then add ARIA for missing states such as expanded, invalid, modal, or live updates.
Use a Safe Claude Code Prompt
Good accessibility prompting reads like a review checklist. It tells Claude Code what it may touch, what it must not touch, what standard to use, and what evidence to return.
claude <<'PROMPT'
Scope:
- Review only src/components/CheckoutForm.tsx and its tests.
- Do not change pricing copy, analytics events, or unrelated styles.
Accessibility target:
- Use WCAG 2.2 AA as the practical target.
- Prefer semantic HTML before ARIA.
- Add ARIA only when native HTML cannot express the state.
Check these items:
- Labels, descriptions, required state, and validation errors.
- Keyboard operation with Tab, Shift+Tab, Enter, Space, and Escape.
- Focus order, visible focus, and focus return after closing UI.
- Color contrast and non-color error indicators.
- Automated axe check plus manual screen-reader notes.
Output:
- Findings first, with file and line references.
- Minimal patch.
- Commands to verify.
- Any remaining risk.
PROMPT
The important phrase is “findings first.” It makes Claude Code explain the defect before rewriting code. That is useful when the change affects a monetized page, a signup flow, or a support form where a broad refactor can break tracking or conversion copy. The same scoped-prompt habit is covered in Claude Code productivity tips.
Use Case 1: Product CTA or Article CTA
A landing page CTA is usually where accessibility and monetization meet. The UI may look like a card, but the behavior is often just navigation. In that case the accessible primitive is an anchor, not a clickable container with JavaScript.
This version looks clickable to mouse users but is weak for keyboard and assistive technology users:
<div class="hero-card" onclick="location.href='/en/products'">
<div class="title">Claude Code Templates</div>
<div class="button">Buy now</div>
</div>
Start by restoring meaning. Give the section a heading, use text that explains the outcome, and make the destination a real link.
<section aria-labelledby="templates-heading" class="product-cta">
<h2 id="templates-heading">Shorten reviews with Claude Code templates</h2>
<p>
Copy reusable prompts for implementation, review, debugging,
and documentation work.
</p>
<a class="primary-link" href="/en/products">
View product resources
</a>
</section>
Ask Claude Code to preserve tracking attributes and CTA destinations while changing markup. Accessibility improvements should not silently remove revenue measurement. For content sites, that means checking article links, Gumroad links, and consultation links after the patch.
Use Case 2: Contact or Consultation Form
Forms are the easiest place to create an accessibility problem that also hurts revenue. A user who cannot identify the field, understand the required format, or hear the validation error is unlikely to submit the form.
The following React component is intentionally small enough to paste into a project. It connects labels, help text, invalid state, and error messages with the relevant input.
import { FormEvent, useState } from "react";
type Errors = {
name?: string;
email?: string;
};
export function ConsultationForm() {
const [errors, setErrors] = useState<Errors>({});
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const data = new FormData(event.currentTarget);
const nextErrors: Errors = {};
if (!String(data.get("name") || "").trim()) {
nextErrors.name = "Enter your name.";
}
if (!String(data.get("email") || "").includes("@")) {
nextErrors.email = "Enter a valid email address.";
}
setErrors(nextErrors);
}
return (
<form aria-labelledby="consultation-title" onSubmit={handleSubmit} noValidate>
<h2 id="consultation-title">Consultation request</h2>
<div className="field">
<label htmlFor="name">Name</label>
<input
id="name"
name="name"
autoComplete="name"
aria-invalid={errors.name ? "true" : "false"}
aria-describedby={errors.name ? "name-error" : undefined}
/>
{errors.name && (
<p id="name-error" role="alert">
{errors.name}
</p>
)}
</div>
<div className="field">
<label htmlFor="email">Email</label>
<p id="email-help">Use an address where we can reply.</p>
<input
id="email"
name="email"
type="email"
autoComplete="email"
aria-invalid={errors.email ? "true" : "false"}
aria-describedby={
errors.email ? "email-help email-error" : "email-help"
}
/>
{errors.email && (
<p id="email-error" role="alert">
{errors.email}
</p>
)}
</div>
<button type="submit">Send request</button>
</form>
);
}
The common failure is connecting aria-describedby to an error element that is not rendered yet, or showing the error without connecting it to the input. Claude Code often produces one of those when asked for validation quickly. The prompt should require it to inspect the accessible name, description, invalid state, and submitted error flow.
Use Case 3: Modal, Command Palette, and Menu
Dialogs, command palettes, settings drawers, and account menus need more than labels. They need predictable keyboard behavior. The WAI-ARIA modal dialog pattern describes the expected behavior: focus moves into the dialog, Tab stays inside it, Escape closes it, and focus returns to the element that opened it.
Here is a minimal React modal foundation. It is not a complete design system component, but it is a useful starting point for Claude Code review because the focus logic is visible.
import { ReactNode, useEffect, useRef } from "react";
type ModalProps = {
open: boolean;
title: string;
onClose: () => void;
children: ReactNode;
};
const focusableSelector = [
"a[href]",
"button:not([disabled])",
"input:not([disabled])",
"select:not([disabled])",
"textarea:not([disabled])",
'[tabindex]:not([tabindex="-1"])',
].join(",");
export function AccessibleModal(props: ModalProps) {
const { open, title, onClose, children } = props;
const dialogRef = useRef<HTMLDivElement>(null);
const previousFocusRef = useRef<HTMLElement | null>(null);
useEffect(() => {
if (!open) return;
previousFocusRef.current = document.activeElement as HTMLElement;
const dialog = dialogRef.current;
const focusable = dialog?.querySelectorAll<HTMLElement>(focusableSelector);
focusable?.[0]?.focus();
function onKeyDown(event: KeyboardEvent) {
if (event.key === "Escape") onClose();
if (event.key !== "Tab" || !dialogRef.current) return;
const items = [...dialogRef.current.querySelectorAll<HTMLElement>(
focusableSelector
)];
const first = items[0];
const last = items[items.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last?.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first?.focus();
}
}
document.addEventListener("keydown", onKeyDown);
return () => {
document.removeEventListener("keydown", onKeyDown);
previousFocusRef.current?.focus();
};
}, [open, onClose]);
if (!open) return null;
return (
<div className="modal-backdrop">
<div
ref={dialogRef}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
className="modal-panel"
>
<h2 id="modal-title" tabIndex={-1}>
{title}
</h2>
{children}
<button type="button" onClick={onClose}>
Close
</button>
</div>
</div>
);
}
For dropdowns, check whether you really need an ARIA menu. The APG menu button pattern is appropriate for command menus, but normal site navigation is often better as a nav element with anchors. Overusing role="menu" changes keyboard expectations and can make a simple navigation harder to use.
Color, Focus, and Mobile Targets
Color issues are easy to introduce during visual polish. The most common regression is removing the focus outline, using pale gray text, or showing error state by color alone. Claude Code should be asked to preserve visible focus and add non-color indicators.
.primary-link {
background: #0f766e;
border-radius: 6px;
color: #ffffff;
display: inline-flex;
font-weight: 700;
gap: 0.5rem;
min-height: 44px;
padding: 0.75rem 1rem;
}
.primary-link:focus-visible,
button:focus-visible,
input:focus-visible {
outline: 3px solid #f59e0b;
outline-offset: 3px;
}
.field [role="alert"] {
border-left: 4px solid #b91c1c;
color: #7f1d1d;
margin-top: 0.5rem;
padding-left: 0.75rem;
}
Mobile matters here. WCAG 2.2 adds target-size guidance, and many real failures happen on touch devices: small close buttons, overlapping sticky headers, or focus indicators hidden under fixed toolbars. Ask Claude Code to check both desktop and mobile viewport behavior when the component is responsive.
Automated axe Check and Manual Screen-Reader Check
axe is useful because it catches structural problems quickly. It does not prove that the label text is useful, the reading order makes sense, or the workflow is understandable. Treat it as a gate, not the whole audit.
npm install -D @axe-core/playwright @playwright/test
npx playwright install --with-deps chromium
import AxeBuilder from "@axe-core/playwright";
import { expect, test } from "@playwright/test";
test("consultation form has no serious accessibility issues", async ({ page }) => {
await page.goto("/contact");
const results = await new AxeBuilder({ page })
.include("main")
.withTags(["wcag2a", "wcag2aa", "wcag22aa"])
.analyze();
expect(results.violations).toEqual([]);
});
name: accessibility-check
on:
pull_request:
jobs:
axe:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npm run build
- run: npx playwright test accessibility.spec.ts
Manual checks should cover the revenue and support paths first: product CTA, signup form, consultation form, checkout, modal, navigation, and error recovery. On Windows, NVDA is a practical starting point. On macOS, VoiceOver is available by default. You do not need a perfect full-site audit before every small release, but you do need to test the paths your change touches.
Concrete Failure Modes to Ask Claude Code About
Use this list as a review prompt after every accessibility patch:
- A
divhasrole="button"but does not support Enter and Space. - An icon-only button has no accessible name.
- An image has
alt="image"or a duplicated caption instead of useful alternative text. aria-hidden="true"hides the dialog or live region by accident.- Validation errors are visible but not connected with
aria-describedby. - Focus outline is removed without a replacement.
- A modal closes visually but focus does not return to the trigger.
- The automated scan runs only on the logged-out marketing page, not the real form.
These are not theoretical. They are the defects that survive when the team relies on visual review only. Claude Code is good at checking them if you make the failure modes explicit.
Release Checklist
Before publishing an accessibility refresh, walk through this checklist:
- Heading order is logical from the page title down.
- Links navigate and buttons change state or submit actions.
- The core workflow works with Tab, Shift+Tab, Enter, Space, and Escape.
- Focus is always visible.
- Dialog focus enters, cycles, closes, and returns correctly.
- Form labels, hints, required state, and errors are announced.
- Error state is not communicated by color alone.
- axe or an equivalent tool reports no serious violations in the changed area.
- Screen-reader output makes sense for CTA, form, and error flows.
- Pricing copy, analytics events, and CTA destinations are unchanged unless intended.
If you want these checks to run automatically after Claude Code edits, the Claude Code hooks guide shows how to turn repeated commands into a workflow.
CTA: Turn the Checklist Into a Reusable Workflow
Do not rewrite this prompt from scratch every week. Start with the free cheat sheet for daily Claude Code command habits, then use prompt templates when review and debugging prompts repeat, and move to consultation when a team needs rollout rules, permissions, and verification receipts.
Hands-On Verification Note
For this refresh, I used three common production failures as the test cases: a CTA card implemented as a clickable div, a form whose errors were visible but not announced, and a modal that did not return focus to the opening button. The most reliable Claude Code flow was to ask for findings first, then request the smallest patch, then verify with keyboard navigation and a screen reader. axe helped catch structural issues, but the final judgment still came from manually reading the CTA and error recovery flow.
Free PDF: Claude Code Cheatsheet
Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.
We handle your data with care and never send spam.
Level up your Claude Code workflow
Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.
About the Author
Masa
Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.
Related Posts
Claude Code Permission Safety Ladder: Expand Access Without Losing Control
A beginner-friendly ladder for moving Claude Code from read-only to limited edits, proof commands, and deploy checks.
Claude Code Small PR Proof Pack: Make Tiny Changes Reviewable
A practical proof pack for Claude Code PRs: diff, checks, public URL, CTA path, and rollback note.
Claude Code Review Gate Before Commit: Diff, Tests, Public URL, and CTA Checks
A commit-time review gate for Claude Code work: diff scope, build, public URL, revenue CTA links, missing tests, and unrelated files.
Related Products
50 Battle-Tested Claude Code Prompt Templates
Copy, paste, ship. 50 production-ready prompts.
Use proven prompts for code review, refactoring, testing, documentation, debugging, architecture, and incident response.
The Complete Claude Code Setup & Configuration Guide
From install to team-ready workflow.
A practical guide to installation, CLAUDE.md, hooks, MCP servers, permissions, IDE setup, and CI/CD workflows.