Storybook を導入する: Tailwind CSS と a11y アドオンの設定
ダイアログの開いた状態は、アプリではボタンを押さないと見られません。お知らせが 0 件のときの表示を確かめるには、データを差し替える必要もあります。
Storybook は、コンポーネントを決まった props で描画した状態を story として並べる道具です。「開いている」「お知らせが無い」といった状態を 1 つずつ story にしておけば、アプリを操作せずに見られます。07 章では、同じ story をテストにも使います。
この章で学ぶこと
- 導入コマンドがプロジェクトに足すもの
- Tailwind CSS のクラスを story に効かせる設定
- a11y アドオンが何を検査するか
- CSF 3 の形式で story を書く方法
この章で扱わないこと
| 観点 | 参照先 |
|---|---|
| コンポーネントのカタログを持つ意味 | Button からデザインシステムへ |
| story の中で操作を自動化する play 関数 | 07 Storybook でテストする |
| Storybook の画面の使い方 | Storybook の公式ドキュメント |
導入コマンドを実行する
プロジェクトの直下で、次のコマンドを実行します1。
npm create storybook@latest
実行すると、Storybook が初めてかどうかと、どの構成を入れるかを尋ねられます。構成は「Recommended」と「Minimal」から選びます。Recommended を選ぶと、ドキュメント、テスト、アクセシビリティの機能がまとめて入ります。本ガイドは Recommended の内容で進めます。
質問に答える代わりに、入れる機能をフラグで指定する方法もあります。次のコマンドで、本ガイドと同じ構成が入ります。-- より後ろが Storybook へ渡るフラグです。
npm create storybook@latest -- --features docs test a11y --yes --no-dev --disable-telemetry
| フラグ | 意味 |
|---|---|
--features docs test a11y | 入れる機能です。公式ドキュメントは、Recommended の内容をフラグで選ぶ例としてこの 3 つを挙げています |
--yes | 残りの質問に、すべて既定の答えで進みます |
--no-dev | 導入の直後に Storybook を起動しません |
--disable-telemetry | 利用状況のデータを送信しません |
--features で選べる機能には、ほかに onboarding (初回のチュートリアル) と ai があります。対話で進めて「Storybook が初めて」と答えた場合は、チュートリアルも入ります。
AI のコーディングエージェントから実行すると、導入コマンドはそれを検出して、エージェント向けの動作 (質問を出さず、AI 向けの設定手順を出力する) に切り替わります。通常の導入と同じ動作にするには --no-agent を足します。本ガイドのプロジェクトは、上のコマンドにこのフラグを足して導入しました。
コマンドが足すもの
このコマンドは、プロジェクトのあちこちに手を入れます。何が変わったかを知っておくと、あとで設定を直すときに迷いません。
| 変わる場所 | 内容 |
|---|---|
package.json の scripts | storybook (開発用の起動) と build-storybook (静的ファイルの書き出し) |
package.json の devDependencies | storybook、@storybook/react-vite、アドオン 4 つ、vitest、@vitest/browser-playwright、@vitest/coverage-v8、playwright |
.storybook/main.ts | 読み込む story とアドオンの一覧 |
.storybook/preview.tsx | すべての story に共通する設定 |
vite.config.ts | story をテストとして実行するための、Vitest の設定 |
vitest.shims.d.ts | 型の参照が 1 行だけ入ったファイル |
.gitignore | Storybook のログと、書き出し先の storybook-static |
src/stories/ | サンプルの story とコンポーネント |
最後に、テストで使う Chromium が Playwright 経由でダウンロードされます。
導入が終わったら、しておくことが 3 つあります。
1 つめは、サンプルの削除です。src/stories/ はディレクトリごと消します。本ガイドでは使いませんし、残しておくと npx tsc -b と npm run build がエラーで止まります。サンプルの Button.tsx と Header.tsx が、使っていない React を import していて、Vite のテンプレートが有効にしている noUnusedLocals に当たるためです。
2 つめは、パッケージの版です。導入コマンドは、vitest、@vitest/browser-playwright、@vitest/coverage-v8、playwright、@chromatic-com/storybook の版を、package.json に "latest" と書きます。このままだと、依存を入れ直すたびに入る版が変わりえます。実際に入った版を確かめて、その版に合わせた範囲に書き換えておきます。
npm ls vitest @vitest/browser-playwright @vitest/coverage-v8 playwright @chromatic-com/storybook
手元で入った版と、書き換えたあとの指定は次のとおりです。
| パッケージ | 入った版 | 書き換えたあとの指定 |
|---|---|---|
vitest | 4.1.10 | ^4.1.10 |
@vitest/browser-playwright | 4.1.10 | ^4.1.10 |
@vitest/coverage-v8 | 4.1.10 | ^4.1.10 |
playwright | 1.64.0 | ^1.64.0 |
@chromatic-com/storybook | 5.4.0 | ^5.4.0 |
このとき、npm が配っている Vitest の最新版は 5.0.3 でしたが、入ったのは 4.1.10 でした。本ガイドのテストは、4.1.10 でも 5.0.3 でも、すべて通ることを確かめています。入った版のまま先へ進めて構いません。
Vitest の 3 つのパッケージをいったん外してから、版を指定して入れ直します。
npm uninstall vitest @vitest/browser-playwright @vitest/coverage-v8
npm install -D vitest@^5.0.3 @vitest/browser-playwright@^5.0.3 @vitest/coverage-v8@^5.0.3
外さずに、package.json の版だけを ^5.0.3 に書き換えて npm install を実行すると、手元では依存の解決に失敗しました (ERESOLVE)。エラーの内容は、Vitest の関連パッケージが互いの版を完全一致で要求している、というものでした。
3 つめは、Linux で動かす場合の Chromium です。導入コマンドは Chromium の本体を入れますが、Chromium が使う OS 側のライブラリまでは入れません。足りないと、07 章でテストを実行したときに Chromium が起動しません。導入の最後に出るメッセージが、次のコマンドを案内しています。
npx playwright install chromium --with-deps
main.ts を確かめる
.storybook/main.ts は、Storybook 本体の設定です。次のコードは、生成された内容を整理したものです。story を探すパターンから、本ガイドで使わない .mdx と JavaScript の拡張子を外し、コメントを足しています。
import type { StorybookConfig } from '@storybook/react-vite'
const config: StorybookConfig = {
// story として読み込むファイル
stories: ['../src/**/*.stories.@(ts|tsx)'],
addons: [
// Chromatic と連携して、見た目の差分を検知する
'@chromatic-com/storybook',
// story を Vitest のテストとして実行する
'@storybook/addon-vitest',
// アクセシビリティを自動で検査する
'@storybook/addon-a11y',
// story からドキュメントのページを作る
'@storybook/addon-docs',
],
// React と Vite の組み合わせ用。プロジェクトの vite.config.ts を読むので、Tailwind CSS のプラグインも story に効く
framework: '@storybook/react-vite',
}
export default config
上のフラグで導入すると、アドオンは 4 つ入ります。
| アドオン | 役割 | 扱う章 |
|---|---|---|
@storybook/addon-docs | story からドキュメントのページを作ります | 本ガイドでは扱いません |
@storybook/addon-a11y | アクセシビリティを自動で検査します | この章と 07 章 |
@storybook/addon-vitest | story を Vitest のテストとして実行します | 07 章 |
@chromatic-com/storybook | 見た目の差分を検知するサービス Chromatic と連携します | 07 章 |
framework の @storybook/react-vite は、プロジェクトの vite.config.ts を読み込みます。03 章で足した Tailwind CSS のプラグインは、Storybook の中でもそのまま働きます。Storybook のために Tailwind CSS の設定をもう一度書く必要はありません。
.storybook を型検査の対象に入れる
TypeScript の設定に、落とし穴が 1 つあります。tsconfig.app.json の include に .storybook とディレクトリ名だけを書いても、中のファイルは型検査の対象になりませんでした。src のようなふつうのディレクトリは名前だけで対象になるので、同じつもりで書くと見落とします。中のファイルまで届くパターンで書きます。
"include": ["src", ".storybook/**/*", "vitest.setup.ts", "vitest.shims.d.ts"]
vitest.shims.d.ts は導入コマンドが作ったファイルです。vitest.setup.ts は 06 章で作るファイルで、ここで先に対象へ入れています。
この指定が効いていることは、.storybook/preview.tsx にわざと型の誤りを書いて確かめられます。npx tsc -b のエラーの一覧に .storybook/preview.tsx の行が出れば、対象に入っています。
Tailwind CSS を story に当てる
Tailwind CSS のプラグインが働いていても、それだけでは story にスタイルが当たりません。プラグインが処理するのは、どこかから読み込まれた CSS だからです。アプリでは main.tsx が index.css を読み込んでいますが、Storybook はアプリの入口を通りません。
そこで、すべての story に共通する設定を書く .storybook/preview.tsx で、同じ CSS を読み込みます2。
import type { Preview } from '@storybook/react-vite'
// アプリと同じ CSS を読み込む。これが無いと、story の中で Tailwind CSS のクラスが効かない
import '../src/index.css'
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
a11y: {
/*
* アクセシビリティの違反が見つかったときの扱い。
* 'todo' は画面に警告を出すだけ、'error' はテストを失敗させる、'off' は検査しない。
*/
test: 'error',
},
},
}
export default preview
足したのは import の 1 行です。この 1 行が無いと、ダイアログは枠も余白も無い素の HTML として表示されます。Base UI は見た目を持たないので、崩れ方がはっきり出ます。
index.css には、03 章で書いた Base UI 向けの設定も入っています。このうち body への指定は、Storybook の中でもそのまま効きます。#root への指定は効きません。Storybook が story を描画する要素の id は storybook-root で、root ではないためです。本ガイドの story には z-index を持つ要素が無いので、この違いは表示に現れません。
a11y アドオンの設定
@storybook/addon-a11y は、story を開くたびに、描画された DOM を axe-core というライブラリで検査します3。結果は Accessibility パネルに 3 つに分けて表示されます。
| 分類 | 内容 |
|---|---|
| Violations | 規則に違反している箇所 |
| Passes | 検査を通った項目 |
| Inconclusive | 自動では判定できず、人が確かめる必要がある項目 |
上の preview.tsx の a11y.test は、違反が見つかったときの扱いを決める設定です。
| 値 | 扱い |
|---|---|
'off' | 検査しません |
'todo' | 違反を画面に警告として出します。テストは失敗しません |
'error' | 違反があるとテストを失敗させます |
導入コマンドが生成する値は 'todo' です。本ガイドでは 'error' に変えています。Base UI が役割や名前を付けてくれていても、タイトルを置き忘れたり、文字の色を薄くしすぎたりすれば違反になります。そうした取りこぼしを、07 章でテストとして検知できるようにするためです。
自動の検査で見つかるのは、アクセシビリティの問題の一部です。Storybook の公式ドキュメントは、axe-core が自動で検出できるのは WCAG の問題の最大 57% だと紹介しています。キーボードだけで操作できるかどうかのように、自動では判定できないものが残ります。
story を書く
story は、コンポーネントと同じディレクトリに *.stories.tsx という名前で置きます。形式は CSF 3 (Component Story Format 3) です。まず、状態を見るための 3 つを書きます。
import type { Meta, StoryObj } from '@storybook/react-vite'
import { expect, fn, screen, waitFor } from 'storybook/test'
import type { Notification } from '../lib/notifications'
import { NotificationsDialog } from './notifications-dialog'
const notifications: Notification[] = [
{ id: 'n1', title: 'メンテナンスを 10 月 12 日 2 時から行います', read: false },
{ id: 'n2', title: 'レポートの書き出しに CSV を追加しました', read: false },
{ id: 'n3', title: '9 月分の請求書を発行しました', read: true },
]
// meta はこのファイルの story 全体に共通する設定
const meta = {
// サイドバーに出る場所
title: 'Components/NotificationsDialog',
component: NotificationsDialog,
// args は story に渡す props の既定値。個々の story で上書きできる
args: {
notifications,
// fn() は「呼ばれたこと」を記録する関数。Actions パネルに出て、play 関数から検証もできる
onMarkAllRead: fn(),
},
} satisfies Meta<typeof NotificationsDialog>
export default meta
type Story = StoryObj<typeof meta>
/** 閉じた状態。開くボタンだけが見える。 */
export const Closed: Story = {}
/** 開いた状態。見た目の確認と、a11y の検査や見た目の比較の対象にする。 */
export const Open: Story = { args: { defaultOpen: true } }
/** お知らせが 1 件も無いとき。 */
export const Empty: Story = { args: { notifications: [], defaultOpen: true } }
上のコードは、07 章で play 関数を足したあとのファイルの前半です。そのため、2 行めの import が完成形になっています。この章の時点で使うのは fn だけなので、2 行めは次のように書いてください。
import { fn } from 'storybook/test'
expect、screen、waitFor を使わないまま import していると、このプロジェクトでは npx tsc -b がエラー (TS6133) で止まります。Vite のテンプレートの tsconfig.app.json が、使っていない変数や import をエラーにする noUnusedLocals を有効にしているためです。3 つの import は、07 章で play 関数と一緒に足します。
このファイルの要素は 3 つです。
| 要素 | 役割 |
|---|---|
meta | ファイル全体に共通する設定です。対象のコンポーネントと、props の既定値 (args) を書きます。default export にします |
Story 型 | StoryObj<typeof meta> から作る型です。story に渡す args が、コンポーネントの props の型と照合されます |
| 名前付きの export | 1 つが 1 つの story になります。args を書くと、meta の既定値をその story だけ上書きします |
meta の末尾の satisfies Meta<typeof NotificationsDialog> は、型を Meta に広げずに、Meta の形に合っているかだけを検査する書き方です。args に書いた props の情報が型に残るので、個々の story では必須の props を省略できます。Closed が空のオブジェクトで済んでいるのはそのためです。
onMarkAllRead に渡している fn() は、呼ばれたことを記録する関数です。story の画面でボタンを押すと、Actions パネルに呼び出しが表示されます。
開いた状態を story にする
Open と Empty は、defaultOpen: true を渡して、最初から開いた状態で描画しています。03 章でコンポーネントに defaultOpen を用意しておいたのはこのためです。
開いた状態を story にしておく利点は 2 つあります。
- 見た目の確認が速くなります。余白や色を直すたびに、ボタンを押して開き直す必要がありません
- 開いた状態が検査の対象になります。a11y アドオンも、07 章の見た目の比較も、story が描画した状態を見ます。閉じた状態の story しか無ければ、ダイアログの中身は一度も検査されません
ダイアログ本体は Portal で body の直下に描画されますが、story の画面にはそのまま表示されます。Storybook のために Portal の出し先を変える必要はありません。
起動して確かめる
開発用のサーバーを起動します。
npm run storybook
ブラウザで http://localhost:6006 を開くと、サイドバーに「Components」と「NotificationsDialog」が並び、その下に 3 つの story が出ます。確かめる点は次のとおりです。
Closedでボタンに枠と余白が付いているか。付いていなければ、preview.tsxの CSS の読み込みが効いていませんOpenとEmptyで、ダイアログが画面の中央に、背景の幕の手前に出ているか- Accessibility パネルの Violations が 0 件か
静的なファイルとして書き出すには、npm run build-storybook を実行します。出力先は storybook-static です。07 章の Chromatic は、この出力を使います。
まとめ
- 導入は
npm create storybook@latestで行います。設定ファイルのほかに、vite.config.tsとpackage.jsonも書き換わります - 導入後に、サンプルの
src/stories/を消し、package.jsonの"latest"を版の範囲に直し、.storybookを型検査の対象に入れます - Tailwind CSS を story に当てるには、
.storybook/preview.tsxでアプリと同じ CSS を読み込みます - a11y アドオンは axe-core で DOM を検査します。
a11y.testを'error'にすると、違反をテストの失敗にできます - story は CSF 3 で書きます。
metaに共通の設定を、名前付きの export に個々の状態を書きます - 開いた状態を story にしておくと、その状態が見た目と a11y の検査の対象になります
次に読む
- 05 役割でテストする — テストを書く前に、何を手がかりに要素を探すかを決めます
- 07 Storybook でテストする — この章の story に操作と検証を足します