Tips & Tricks (Updated: 6/2/2026)

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.

Claude Code Accessibility Workflow: Semantic HTML, ARIA, axe, and Manual Checks

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.

AreaPractical baselineCommon failure
Semantic HTMLUse button, a, form, label, main, and nav for their real purposeClickable div elements replace native controls
KeyboardTab, Shift+Tab, Enter, Space, and Escape cover the main workflowA modal can only be closed with a mouse
FocusFocus enters new UI and returns to the trigger when it closesFocus escapes behind a dialog
ARIAAdd it only when native HTML cannot express the statearia-label hides missing visible labels
ContrastText, controls, errors, and focus indicators are visibleLow-contrast gray text passes design review but not use
FormsLabels, hints, required state, and errors are connected to inputsErrors appear visually but are not announced
TestingUse axe plus manual keyboard and screen-reader checksThe 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 div has role="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:

  1. Heading order is logical from the page title down.
  2. Links navigate and buttons change state or submit actions.
  3. The core workflow works with Tab, Shift+Tab, Enter, Space, and Escape.
  4. Focus is always visible.
  5. Dialog focus enters, cycles, closes, and returns correctly.
  6. Form labels, hints, required state, and errors are announced.
  7. Error state is not communicated by color alone.
  8. axe or an equivalent tool reports no serious violations in the changed area.
  9. Screen-reader output makes sense for CTA, form, and error flows.
  10. 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.

#Claude Code #accessibility #WCAG #a11y #React
Free

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.

Masa

About the Author

Masa

Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.