How to Streamline Accessibility with Claude Code
Learn how to streamline accessibility using Claude Code. Includes practical code examples and step-by-step guidance.
Why Use Claude Code for Accessibility
Web accessibility is critical, but understanding WCAG standards and the correct usage of ARIA attributes is complex. Claude Code can generate code that follows accessibility best practices and efficiently audit existing code.
Auditing Existing Code for Accessibility
> Review every component under src/components/ for accessibility.
> Check against WCAG 2.1 AA and list issues along with suggested fixes.
Claude Code can detect issues like:
- Images missing
altattributes - Form elements not associated with
labelelements - Insufficient color contrast ratios
- Interactive elements that cannot be operated by keyboard
- Incorrect usage of ARIA attributes
Generating Accessible Components
Modal Dialog
> Create an accessible modal dialog.
> Implement focus trap, close on ESC, and background scroll lock.
import { useEffect, useRef, useCallback } from 'react';
interface ModalProps {
isOpen: boolean;
onClose: () => void;
title: string;
children: React.ReactNode;
}
function Modal({ isOpen, onClose, title, children }: ModalProps) {
const modalRef = useRef<HTMLDivElement>(null);
const previousFocus = useRef<HTMLElement | null>(null);
// Focus trap
const trapFocus = useCallback((e: KeyboardEvent) => {
if (!modalRef.current) return;
const focusable = modalRef.current.querySelectorAll<HTMLElement>(
'a[href], button:not([disabled]), input:not([disabled]), select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
if (e.key === 'Escape') onClose();
}, [onClose]);
useEffect(() => {
if (isOpen) {
previousFocus.current = document.activeElement as HTMLElement;
document.body.style.overflow = 'hidden';
document.addEventListener('keydown', trapFocus);
// Focus the first focusable element inside the modal
setTimeout(() => {
modalRef.current?.querySelector<HTMLElement>('[autofocus], button')?.focus();
}, 0);
}
return () => {
document.body.style.overflow = '';
document.removeEventListener('keydown', trapFocus);
previousFocus.current?.focus();
};
}, [isOpen, trapFocus]);
if (!isOpen) return null;
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center"
role="presentation"
>
<div
className="fixed inset-0 bg-black/50"
aria-hidden="true"
onClick={onClose}
/>
<div
ref={modalRef}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
className="relative z-10 mx-4 w-full max-w-lg rounded-lg bg-white p-6 shadow-xl dark:bg-gray-800"
>
<h2 id="modal-title" className="text-xl font-bold mb-4">
{title}
</h2>
{children}
<button
onClick={onClose}
aria-label="Close"
className="absolute right-4 top-4 rounded-full p-1 hover:bg-gray-100 dark:hover:bg-gray-700"
>
<svg aria-hidden="true" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" />
</svg>
</button>
</div>
</div>
);
}
Accessible Dropdown Menu
function DropdownMenu({ label, items }: { label: string; items: MenuItem[] }) {
const [isOpen, setIsOpen] = useState(false);
const [activeIndex, setActiveIndex] = useState(-1);
const menuRef = useRef<HTMLUListElement>(null);
const handleKeyDown = (e: React.KeyboardEvent) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setActiveIndex(prev => Math.min(prev + 1, items.length - 1));
break;
case 'ArrowUp':
e.preventDefault();
setActiveIndex(prev => Math.max(prev - 1, 0));
break;
case 'Enter':
case ' ':
e.preventDefault();
if (activeIndex >= 0) items[activeIndex].onClick();
setIsOpen(false);
break;
case 'Escape':
setIsOpen(false);
break;
}
};
return (
<div className="relative" onKeyDown={handleKeyDown}>
<button
aria-haspopup="true"
aria-expanded={isOpen}
onClick={() => setIsOpen(!isOpen)}
>
{label}
</button>
{isOpen && (
<ul ref={menuRef} role="menu" className="absolute mt-1 rounded-md border bg-white shadow-lg dark:bg-gray-800">
{items.map((item, i) => (
<li
key={i}
role="menuitem"
tabIndex={-1}
className={`cursor-pointer px-4 py-2 ${i === activeIndex ? 'bg-blue-100 dark:bg-blue-900' : ''}`}
onClick={item.onClick}
>
{item.label}
</li>
))}
</ul>
)}
</div>
);
}
Form Accessibility
> Rebuild the contact form so it is accessible.
> Make sure validation errors are also announced to screen readers.
function ContactForm() {
const [errors, setErrors] = useState<Record<string, string>>({});
return (
<form aria-label="Contact form" noValidate>
<div className="mb-4">
<label htmlFor="name" className="block font-medium mb-1">
Name <span aria-label="required">*</span>
</label>
<input
id="name"
type="text"
required
aria-required="true"
aria-invalid={!!errors.name}
aria-describedby={errors.name ? 'name-error' : undefined}
className="w-full rounded border p-2"
/>
{errors.name && (
<p id="name-error" role="alert" className="mt-1 text-sm text-red-600">
{errors.name}
</p>
)}
</div>
<div className="mb-4">
<label htmlFor="email" className="block font-medium mb-1">
Email <span aria-label="required">*</span>
</label>
<input
id="email"
type="email"
required
aria-required="true"
aria-invalid={!!errors.email}
aria-describedby="email-hint email-error"
className="w-full rounded border p-2"
/>
<p id="email-hint" className="mt-1 text-xs text-gray-500">
e.g., [email protected]
</p>
{errors.email && (
<p id="email-error" role="alert" className="mt-1 text-sm text-red-600">
{errors.email}
</p>
)}
</div>
<button type="submit" className="rounded bg-blue-600 px-4 py-2 text-white">
Submit
</button>
</form>
);
}
Adding Automated Tests
> Add accessibility tests using jest-axe.
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
describe('Modal accessibility', () => {
it('should have no accessibility violations', async () => {
const { container } = render(
<Modal isOpen={true} onClose={() => {}} title="Test Modal">
<p>Content</p>
</Modal>
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
Summary
With Claude Code, you can efficiently generate accessible components that meet WCAG standards and audit existing code. Configuring hooks to automatically run accessibility tests is another effective approach. For details, see the hooks guide. Tips for folding accessibility into your daily workflow are covered in productivity tips that 3x your output.
For more on Claude Code, see the official Anthropic docs. For WCAG guideline details, see W3C WCAG 2.1.
Free PDF: Claude Code Cheatsheet in 5 Minutes
Just enter your email and we'll send you the single-page A4 cheatsheet right away.
We handle your data with care and never send spam.
Level up your Claude Code workflow
50 battle-tested prompt templates you can copy-paste into Claude Code right now.
About the Author
Masa
Engineer obsessed with Claude Code. Runs claudecode-lab.com, a 10-language tech media with 2,000+ pages.
Related Posts
7 CLAUDE.md Templates for Claude Code You Can Copy Into Real Projects
Copy-paste 7 practical CLAUDE.md templates for solo apps, content sites, APIs, teams, and legacy codebases, plus the failure cases to avoid.
Claude Code Approval and Sandbox Guide | Safe Daily Settings for Real Work
Learn how to split Claude Code actions into allow, ask, deny, and sandboxed workflows with working settings, hooks, and rollout examples.
Complete Beginner's Guide to Claude Code 2026 | 7 Steps from Zero to Production-Ready
A complete beginner's guide for first-time Claude Code users. From installation to integrating it into your real development workflow — covering every pitfall Masa ran into when starting out.
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.