Tips & Tricks

Claude Codeでアクセシビリティ対応を効率化する方法

Claude Codeを活用してWebアクセシビリティ(WCAG準拠)の実装・監査・修正を効率的に行う方法を解説します。

アクセシビリティ対応にClaude Codeを使うメリット

Webアクセシビリティの対応は重要ですが、WCAG基準の理解やARIA属性の正しい使い方は複雑です。Claude Codeはアクセシビリティのベストプラクティスを踏まえたコード生成と、既存コードの監査を効率的に行えます。

既存コードのアクセシビリティ監査

> src/components/ 配下の全コンポーネントをアクセシビリティ観点でレビューして。
> WCAG 2.1 AA基準でチェックして、問題と修正案を一覧にして。

Claude Codeは以下のような問題を検出します。

  • 画像にalt属性が不足している
  • フォーム要素にlabel要素が関連付けられていない
  • カラーコントラスト比が不足している
  • キーボード操作ができないインタラクティブ要素がある
  • ARIA属性が不適切に使われている

アクセシブルなコンポーネントの生成

モーダルダイアログ

> アクセシブルなモーダルダイアログを作成して。
> フォーカストラップ、ESCキーで閉じる、背景のスクロールロックを実装して。
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);

  // フォーカストラップ
  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);

      // モーダル内の最初のフォーカス可能要素にフォーカス
      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="閉じる"
          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>
  );
}

アクセシブルなドロップダウンメニュー

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>
  );
}

フォームのアクセシビリティ

> お問い合わせフォームをアクセシブルに作り直して。
> バリデーションエラーもスクリーンリーダーに伝わるようにして。
function ContactForm() {
  const [errors, setErrors] = useState<Record<string, string>>({});

  return (
    <form aria-label="お問い合わせフォーム" noValidate>
      <div className="mb-4">
        <label htmlFor="name" className="block font-medium mb-1">
          お名前 <span aria-label="必須">*</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">
          メールアドレス <span aria-label="必須">*</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">
          例: [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">
        送信
      </button>
    </form>
  );
}

自動テストの導入

> 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();
  });
});

まとめ

Claude Codeを使えば、WCAG基準に沿ったアクセシブルなコンポーネントの生成から既存コードの監査まで効率的に行えます。フック機能でアクセシビリティテストを自動実行する設定も有効です。詳しくはフック機能ガイドを参照してください。日々の開発フローに組み込むコツは生産性を3倍にするTipsで紹介しています。

Claude Codeの詳細はAnthropic公式ドキュメントをご覧ください。WCAGガイドラインの詳細はW3C WCAG 2.1を参照してください。

#Claude Code #アクセシビリティ #WCAG #a11y #React