Storybookのa11yアドオンをClaude Codeで回し、違反を直す手順
Storybookのaddon-a11yとVitestアドオンでストーリー単位のaxe検査をCIで落とし、違反の出たコンポーネントだけClaude Codeに直させるStopフックと運用の手順です。
Storybookの @storybook/addon-a11y は、ストーリーごとにaxe-coreを走らせ、違反をストーリー単位で報告します。test: 'error' を付けたストーリーは、Vitestで実行するとテストとして失敗します。つまり「どのコンポーネントのどのストーリーが違反したか」が、そのまま失敗したテスト名で分かります。
この粒度は、Claude Codeに修正を任せるのに向いています。ページ全体の検査だと、違反がどのコンポーネント由来か探すところから始まります。ストーリー単位なら、失敗したストーリーの *.stories.tsx を手がかりに、直す範囲がほぼ決まります。
この記事は、検査の組み方、違反を段階的に減らす test パラメータの使い方、そしてStopフックで「違反が残ったまま終わらせない」構成までを順に作ります。
addon-a11yとVitestアドオンでストーリー単位の検査を出す
Accessibility(a11y)アドオンは、Dequeが公開するaxe-coreをStorybookに載せたものです。導入は1コマンドで済みます。
npx storybook add @storybook/addon-a11y
npx storybook add @storybook/addon-vitest1行目がa11yアドオンの導入と設定、2行目がVitestアドオンとVitestの導入と設定です。Storybook側の説明では、axe-coreは自動検出でWCAG違反の最大57%を拾えるとされています。残りは人の目で見る領域です。
Vitestアドオンを入れる理由は、CLIとCIで検査を回せるからです。ストーリーをVitestのテストに変換して、Playwrightで用意したChromiumのブラウザモードで実行します。Storybook本体を起動しておく必要はありません。
package.json には、Storybook分だけを実行するスクリプトを足しておきます。
{
"scripts": {
"test": "vitest",
"test-storybook": "vitest --project=storybook"
}
}--project=storybook は、Storybook用のVitestプロジェクトだけを動かす指定です。npm run test-storybook はウォッチモードで起動するのが既定なので、Claude CodeやCIから呼ぶときは、後述のとおり一度きりの実行にします。
Vitestアドオンは、Vite系のフレームワークで使えます。Vite以外のビルダーを使っているプロジェクトでは、test-runnerを使う形になります。test-runnerは実行中または公開済みのStorybookが必要で、a11yの検査自体は両方で動きます。
test パラメータで、落とす範囲を決める
a11yアドオンの結果をテストの合否にするかどうかは、parameters.a11y.test で決まります。値は3つです。
| 値 | 動き |
|---|---|
'off' | 動き検査を回さない。アドオンパネルでは手動で確認できる |
'todo' | 動き検査は回る。違反はStorybookのUIに警告として出る |
'error' | 動き違反でテストが失敗する。CLIとCIでも落ちる |
todo は「警告」ではなく、コード上のTODOとして付ける値です。名前のとおり、違反を承知で後回しにしたストーリーの目印になります。
ここで落とし穴が1つあります。todo にしたストーリーは、CIでエラーも警告も出力も出ません。ローカルでStorybookのUIから実行したときだけ警告として見えます。todo を付けた時点で、そのストーリーはCI上では「静かな」状態になる点に注意が要ります。
test はプロジェクト全体、ファイル単位(meta)、ストーリー単位のどこにも書けます。狭い方が優先されます。
// .storybook/preview.ts
import type { Preview } from '@storybook/react-vite';
const preview: Preview = {
parameters: {
// 全ストーリーで違反をテスト失敗にする
a11y: { test: 'error' },
},
};
export default preview;@storybook/react-vite の部分は、使っているフレームワークの名前に置き換えます。
既存の違反は todo で棚上げし、1コンポーネントずつ消す
既存のコンポーネントに test: 'error' を一斉に当てると、初日からCIが赤くなります。Storybook側が勧めるのは、次の段階導入です。
違反を減らす段階導入
- 1
全体を error にする
.storybook/preview.tsにa11y: { test: 'error' }を置きます。新しく書くストーリーはここで検査の対象になります。 - 2
落ちたコンポーネントを todo にして基準線を作る
失敗したコンポーネントの
metaにa11y: { test: 'todo' }を足します。ここでコミットしておくと、改善の出発点が残ります。 - 3
Buttonのような基礎部品から直す
他のコンポーネントの中で使われる部品から着手します。アドオンパネルの修正案を手がかりに直し、
todoを外します。 - 4
残りも同じ手順で繰り返す
すべての
todoが消えるまで続けます。
この「1コンポーネントずつ todo を外す」単位は、Claude Codeへの依頼の単位にそのまま使えます。次の節から、その依頼を自動で回す仕掛けを作ります。
Stopフックで、違反が残ったまま終わらせない
Claude Codeには、応答を終えようとした時点で動くStopフックがあります。フックがJSONで decision: "block" と reason を返すと、Claudeは止まらずに reason を次の指示として受け取って作業を続けます。終了コード2で止める場合は、標準エラー出力が理由として渡されます。
これを使い、Claudeがコンポーネントを触り終えるたびにStorybookの検査を走らせて、違反が出ていれば要約を返すスクリプトを作ります。
#!/bin/bash
# .claude/hooks/storybook-a11y.sh
INPUT=$(cat)
# すでにフックの指示で続行中なら、無限ループを避けて終える
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0
fi
cd "$CLAUDE_PROJECT_DIR" || exit 0
# コンポーネントかストーリーに変更がなければ検査しない
if ! git status --porcelain | grep -qE '\.(stories\.)?(tsx|jsx|vue|svelte)$'; then
exit 0
fi
OUT=$(npm run test-storybook -- --run 2>&1)
STATUS=$?
if [ $STATUS -eq 0 ]; then
exit 0
fi
# 失敗したテストの行だけを渡して、長い出力でコンテキストを食わない
SUMMARY=$(echo "$OUT" | grep -E 'FAIL|✗|×|violation|Error' | head -40)
jq -n --arg r "Storybookのa11y検査が失敗しています。違反の出たコンポーネントだけ修正し、npm run test-storybook -- --run で再確認してください。
$SUMMARY" '{decision: "block", reason: $r}'スクリプトの要点は3つです。
stop_hook_activeがtrueのときはすぐ終える。フックが続行させた応答でさらにブロックし続けるのを防ぎます。Claude Codeは、連続8回ブロックしても間にツール呼び出しがなければ、次のブロックを上書きして終わらせます。上限を変えたいときはCLAUDE_CODE_STOP_HOOK_BLOCK_CAPを使います。- 変更のないときは検査しない。コンポーネントに触れていない会話のたびに、ブラウザモードのテストを走らせる無駄をなくします。
- 失敗の要約だけをreasonに入れる。
head -40は目安の長さで、プロジェクトの出力に合わせて調整します。
登録は .claude/settings.json で行います。
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/storybook-a11y.sh",
"timeout": 300
}
]
}
]
}
}timeout は秒単位で、command フックの既定は600秒です。ブラウザの起動を含めて余裕を見込み、ここでは300秒にしました。短くしすぎると、検査の途中で打ち切られます。
フックの出力は、実際のプロジェクトでは grep のパターンが合わないことがあります。最初の1回は、npm run test-storybook -- --run を手で実行し、失敗時にどんな行が出るかを見てから、grep の条件を決めてください。ここを雑にすると、要約が空になってClaudeに何も伝わりません。
Claudeに直す手順を渡すCLAUDE.md
フックが「失敗した」と伝えても、直し方の流儀は別に決めておく必要があります。CLAUDE.mdに、次のような規約を置いておきます。
## Storybookのa11y検査
- 検査は `npm run test-storybook -- --run`。失敗したストーリーのコンポーネントだけ直す
- 直す前に、アドオンパネルの修正案に相当する内容を、失敗した規則IDから判断する
- `a11y: { test: 'todo' }` を新しく足さない。外すのはよい
- `image-alt` などの規則を `enabled: false` で無効化しない。除外が必要なら理由を書いて相談する
- 修正は1コンポーネントずつ。直したら同じコマンドで再実行して通ったことを確かめる2つ目と3つ目が肝です。違反を消す最短の方法は、検査を弱めることです。todo の追加や規則の無効化を禁止しておかないと、Claudeが「テストを通す」ために設定側を書き換える可能性があります。設定を触らせたくなければ、.storybook/preview.ts への編集をPreToolUseフックで止める手もあります。
CIで落として、修正の起点にする
CIでは、Vitestアドオンのテストをそのまま実行します。a11yの失敗は、test: 'error' のストーリーだけがCIを落とします。
name: storybook-a11y
on: pull_request
jobs:
a11y:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npm run test-storybook -- --runブラウザモードのテストはChromiumを必要とするので、playwright install の手順を挟みます。
CIの失敗ログをClaudeに読ませる考え方は、ClaudeでGitHub Actionsのフレークを見分けると同じです。失敗したテスト名の行だけを抜き出して渡すのが基本です。出力にStorybookへのリンクを含めたい場合は、プラグインの storybookUrl オプションに公開先のURLを渡すと、失敗時にそのストーリーへのリンクが出ます。
検査が見つけないもの、つまずきやすい点
最初に押さえておくのは、ストーリー単位の検査が見るのは、描画されたDOMだけという点です。キーボードだけで一連の操作が完走できるか、スクリーンリーダーの読み上げ順が自然かは、axeの自動検査の外にあります。57%という数字は、自動で拾える上限の目安です。全体の網羅ではありません。
つまずきやすい点は次のとおりです。
- 非同期コンポーネントで違反が出ない。SuspenseやReact Server Componentsを使うコンポーネントは、最終的なUIになる前に検査が走ることがあり、違反があっても「違反なし」になる場合があります。Storybook側は
.storybook/main.tsのfeatures.developmentModeForBuild: trueを案内しています。 - 環境によって結果が変わる。Vitestアドオンは、プロジェクトのVitest設定とPlaywrightのChromiumで実行します。ブラウザのバージョンや設定の差で、axeの結果が環境ごとにずれることがあります。ローカルで通ってCIで落ちるときは、まずブラウザのバージョンを揃えます。
todoは静かに溜まる。CIに出力が出ないため、放置すると減りません。grep -r "test: 'todo'" srcの件数を定期的に見る運用が、現実的な対策です。- フックが走るたびに時間がかかる。ブラウザモードのテストは、起動のたびに時間がかかります。ストーリー全体ではなく、変更したコンポーネントだけに絞る方法は、Vitestの
--projectに加えてファイル名の指定で実現できます。
隣接する手段との使い分け
同じアクセシビリティ検査でも、見る範囲が違います。
| 手段 | 見る単位 | 向くとき |
|---|---|---|
| Storybook addon-a11y | 見る単位ストーリー(コンポーネント) | 向くとき部品の違反を、その部品の中で直したい |
| Playwright + axe | 見る単位ページ・画面遷移 | 向くとき実際の画面で、ページ全体の違反を落としたい |
| Axe Accessibilityプラグイン | 見る単位手元のページ | 向くときClaude Codeの対話の中で、都度検査したい |
ページ全体のPlaywright検査と指紋比較の作り方はClaude CodeでPlaywrightとaxeのアクセシビリティ検査をPRで落とすにあります。プラグインから回す手順はClaude CodeのAxe Accessibilityプラグインでアクセシビリティ検査を回すが扱っています。Storybookのコンポーネント情報をClaudeに渡して新しいUIを作らせる側はStorybook MCPサーバーをClaude Codeに繋ぐ手順の領分です。
コンポーネント単位で基準線を作り、ページ単位で最終確認する組み合わせが、見落としを減らします。部品の段階で Button の aria-label 漏れを直しておけば、ページ検査には同じ違反が現れにくくなります。
まとめ
ストーリー単位の検査は、Claudeに渡す「直す範囲」を最初から絞ってくれます。test: 'error' で新規を守り、todo で既存を棚上げし、Stopフックで違反の残った状態を終わらせない。この3点が揃えば、依頼は「todo を1つ外して通す」という小さな単位になります。
最初の一歩は、Button のような基礎部品1つを選び、todo を外して通すところまでをClaude Codeに任せることです。出力の形と所要時間が分かってから、フックとCIに広げます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。