Tips & Tricks

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 alt attributes
  • Form elements not associated with label elements
  • Insufficient color contrast ratios
  • Interactive elements that cannot be operated by keyboard
  • Incorrect usage of ARIA attributes

Generating Accessible Components

> 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., user@example.com
        </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.

#Claude Code #accessibility #WCAG #a11y #React

Level up your Claude Code workflow

50 battle-tested prompt templates you can copy-paste into Claude Code right now.

Free

Free PDF: Claude Code Cheatsheet in 5 Minutes

Key commands, shortcuts, and prompt examples on a single printable page.

Download PDF
M

About the Author

Masa

Engineer obsessed with Claude Code. Runs claudecode-lab.com, a 10-language tech media with 2,000+ pages.