Claude Codeでa11y対応を可能な限り自動化する
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のハイコントラスト設定への対応 |
まとめ:多層防御の全体像
CLAUDE.mdで基本の指示を与える- ファイル編集のたびにPostToolUse Hook → eslint-plugin-jsx-a11y(静的解析)
- Claudeの応答完了後にStop Hook → @axe-core/playwright(E2E テスト)
- CI / PR 時にLighthouse CI → スコア閾値チェック
- スクリーンリーダー・キーボード操作を人的チェック
詳細な検証は未実施なので、これら全てでなく一部だけでもどんどん導入していきたいです。
参考