Claude Media
Storybookのa11yアドオンをClaude Codeで回し、違反を直す手順

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-vitest

1行目が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. 1

    全体を error にする

    .storybook/preview.ts に a11y: { test: 'error' } を置きます。新しく書くストーリーはここで検査の対象になります。

  2. 2

    落ちたコンポーネントを todo にして基準線を作る

    失敗したコンポーネントの meta に a11y: { test: 'todo' } を足します。ここでコミットしておくと、改善の出発点が残ります。

  3. 3

    Buttonのような基礎部品から直す

    他のコンポーネントの中で使われる部品から着手します。アドオンパネルの修正案を手がかりに直し、todo を外します。

  4. 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に広げます。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/hooks
  2. code.claude.com/docs/en/hooks-guide
  3. storybook.js.org/docs/writing-tests/accessibility-testing
  4. storybook.js.org/docs/writing-tests/integrations/vitest-addon
この記事を共有:XはてブLinkedIn