Claude Codeでa11y対応を可能な限り自動化する

Tech

rules + Hooks + CIの多層構造を作る

アクセシビリティは個人的にはかなり興味のある分野で、以前zennにも記事を投稿しています。[1]

今回はa11y対応をAIコーディングに組み込む方法を検討しました。

Claude Codeの機能とCIを組み合わせて、a11yチェックをできる限り自動化してみます。

TL;DR

CLAUDE.mdに丁寧なルールを書いても、セッションをまたいだり、指示が長くなってくるとClaudeは忘れることがあります。

以下を組み合わせることで、ローカル開発中からCIまで多層的にa11yを担保できる仕組みを作ります。

アプローチ 特徴
CLAUDE.md 指示・方針の文書化。Claudeが読むが、忘れることがある
Hooks ファイル変更のたびに必ず実行される。忘れない
CI(Lighthouse等) プッシュ・PR時の最終ゲート

1. CLAUDE.mdにルールを書く

CLAUDE.mdにアクセシビリティの基本的な規約について記載します。(おすすめは.claude/rules/a11y.mdに書いて、CLAUDE.mdから@参照

これによってセッション開始時に指示を読み込んでくれます。

フロントマターのpathsを使ってフロントエンドのフォルダを指定し、ルールを適用する対象を絞ります。

CLAUDE.mdサンプル
---
paths:
  - "frontend/src/**/*.{tsx,ts}"
---

# アクセシビリティ(a11y)規約

## 基本方針
- 視覚だけでなくキーボード操作でも利用可能にする
- WCAG 2.1 AAに準拠する

## セマンティックHTML
適切なHTMLタグを使用する。インタラクティブ要素にdivを使わない。

## ARIA属性
- 必要な場合のみ使用する
- アイコンボタンにはaria-labelを必ず付与する

## 画像
- すべての画像にalt属性を付与する
- 装飾画像はalt=""を使用する

## フォーム
- labelとinputを必ず関連付ける

## キーボード操作
- すべての操作がキーボードで可能であること
- tabindexの乱用禁止(特にtabindex > 0)

## カラーとコントラスト
- 十分なコントラスト比を確保する(AA: 4.5:1 以上)
- 色だけで情報を伝えない

## フォーカス管理
- フォーカス状態を必ず可視化する
- モーダル表示時はフォーカスを内部に閉じる(フォーカストラップ)

## 見出し構造
- h1 → h2 → h3 の順序を守る
- 見出しレベルを飛ばさない

## 禁止事項
- 非インタラクティブ要素へのクリックイベント付与
- alt属性の欠落
- キーボード操作不可のUI

補足:スタイリング用のrulesとの責務分離

プロジェクトルール(.claude/rules)で複数の指示を管理する場合、スタイリングについての規約(styling.mdなど)も作成する場合があると思います。

スタイル定義についてはa11yと共通する内容も含まれるため、styling.mdは「どう作るか」、a11y.md「どう使えるべきか(ユーザー視点)」という原則を意識して責務を分割します。

ファイル 役割
styling.md 見た目・設計・UI 実装ルール(shadcn/ui の使い方、Tailwind、コンポーネント設計など)
a11y.md ユーザー操作・アクセシビリティ要件(キーボード操作、セマンティクス、ARIA、コントラスト、フォーカス管理など)

2. Hooksでファイル変更時に静的解析を自動実行する

Claude CodeのHooksは、特定のライフサイクルイベント(ツール実行後など)にシェルコマンドを確実に実行する仕組みです。
この「確実に」の部分が重要で、LLMの判断に依存しないため、セッション内で指示を忘れても必ず走る処理になります。

eslint-plugin-jsx-a11yを使った静的解析フックを作成し、ファイル変更毎に実行するようにします。

Hooks設定方法例

設定ファイルの場所

ファイル 用途
.claude/settings.json プロジェクト共有(Git 管理・チーム全員に適用)
.claude/settings.local.json 個人設定(Git管理外)
~/.claude/settings.json ユーザーグローバル設定

チームで共有するa11yチェックは.claude/settings.jsonに書く。

注意: settings.json への変更は即座には反映されない。Claude Codeはセッション起動時にフックのスナップショットを取得し、セッション中はそれを使用する。変更を反映するにはセッションを再起動するか、/hooksコマンドでレビューして適用する。

eslint-plugin-jsx-a11yを使った静的解析フック

まず依存をインストールしておく。

npm install --save-dev eslint-plugin-jsx-a11y

.claude/hooks/a11y-check.shを作成する。

#!/bin/bash
# a11y静的解析チェック(PostToolUse hook)
# Claude Codeはstdin経由でイベントJSONを渡す
 
INPUT=$(cat)  # stdinからJSONを読み込む
file=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
 
# TSX/JSXファイルの場合のみ実行
if [[ "$file" =~ \.(tsx|jsx)$ ]]; then
  echo "🔍 a11y チェック: $file" >&2
  npx eslint "$file" --plugin jsx-a11y --rule 'jsx-a11y/alt-text: error' \
    --rule 'jsx-a11y/aria-props: error' \
    --rule 'jsx-a11y/aria-role: error' \
    --rule 'jsx-a11y/interactive-supports-focus: error' \
    --rule 'jsx-a11y/label-has-associated-control: error' \
    --rule 'jsx-a11y/no-noninteractive-element-interactions: warn' \
    --rule 'jsx-a11y/no-static-element-interactions: error'
fi

実行権限を付与する。

chmod +x .claude/hooks/a11y-check.sh

.claude/settings.jsonに登録する。

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/a11y-check.sh",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

3. @axe-core/playwrightでE2Eテスト内にアクセシビリティチェックを組み込む

eslintの静的解析では「実際にレンダリングされたときの状態」は確認できません。

そこで@axe-core/playwrightを使うと、PlaywrightのE2Eテストの中でアクセシビリティ違反を検出することができます。

npm install --save-dev @axe-core/playwright
テストコード例
// __tests__/a11y.spec.ts
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test.describe('アクセシビリティ', () => {
  test('トップページに重大なa11y違反がないこと', async ({ page }) => {
    await page.goto('/');

    const results = await new AxeBuilder({ page })
      .withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
      .analyze();

    expect(results.violations).toEqual([]);
  });

  test('フォームが正しくラベル付けされていること', async ({ page }) => {
    await page.goto('/contact');

    const results = await new AxeBuilder({ page })
      .include('#contact-form')
      .withTags(['wcag2a'])
      .analyze();

    expect(results.violations).toEqual([]);
  });
});

このテストも忘れないよう、Hooksに組み込んでおきます。
今度はStopイベント(Claudeが応答を終了したとき)にテストを走らせるように設定します。

(カスタムスラッシュコマンドで詳細レビュー + テストを実行する方法もありますが、実行漏れを防ぐためにもHooksに設定する方が◎)

.claude/settings.json
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "npx playwright test tests/a11y.spec.ts --reporter=line 2>&1 | tail -20",
            "timeout": 120,
            "async": true
          }
        ]
      }
    ]
  }
}

"async": trueにするとテストがバックグラウンドで実行されるため、Claude のレスポンスをブロックしない。

ここまでがClaude Codeで仕組み化するまでの設定です。

4. LighthouseでCIに組み込む

ここからはClaude Code外でのチェックです。

ローカルのHooksは個人・開発中の防衛ライン、CIはPRマージ前の最終ゲートという位置づけで、LighthouseをCIに組み込みます。
もしくは通常通りブラウザの開発ツールで実行でもよいと思います。

5. 自動ツールで検出できないものは目視チェック

諸説(?)ありますが、axe-coreやLighthouseが検出できるのは、a11y問題全体の約30〜40%と言われています。

最終的には人の目で確認することも重要だと思います。

チェック項目 理由
スクリーンリーダーでの読み上げ確認(VoiceOver / NVDA) 実際の読み上げ内容・順序は自動検出困難
キーボードのみでの操作フロー フォーカス移動が自然かどうかは文脈依存
altテキストの意味的な適切さ alt=""ではなく意味のある説明になっているか
拡大表示(200%)での崩れ確認 レイアウトの破綻は視覚的にのみ判断可能
ハイコントラストモードでの表示確認 OSのハイコントラスト設定への対応

まとめ:多層防御の全体像

  1. CLAUDE.mdで基本の指示を与える
  2. ファイル編集のたびにPostToolUse Hook → eslint-plugin-jsx-a11y(静的解析)
  3. Claudeの応答完了後にStop Hook → @axe-core/playwright(E2E テスト)
  4. CI / PR 時にLighthouse CI → スコア閾値チェック
  5. スクリーンリーダー・キーボード操作を人的チェック

詳細な検証は未実施なので、これら全てでなく一部だけでもどんどん導入していきたいです。

参考

https://azukiazusa.dev/blog/axe-core-playwright/
https://docs.anthropic.com/en/docs/claude-code/hooks
https://www.w3.org/TR/WCAG21/

脚注
  1. https://zenn.dev/tmkst/articles/7a806e10b72a40 ↩︎