メインコンテンツまでスキップ

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 の scriptsstorybook (開発用の起動) と build-storybook (静的ファイルの書き出し)
package.json の devDependenciesstorybook、@storybook/react-vite、アドオン 4 つ、vitest、@vitest/browser-playwright、@vitest/coverage-v8、playwright
.storybook/main.ts読み込む story とアドオンの一覧
.storybook/preview.tsxすべての story に共通する設定
vite.config.tsstory をテストとして実行するための、Vitest の設定
vitest.shims.d.ts型の参照が 1 行だけ入ったファイル
.gitignoreStorybook のログと、書き出し先の 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

手元で入った版と、書き換えたあとの指定は次のとおりです。

パッケージ入った版書き換えたあとの指定
vitest4.1.10^4.1.10
@vitest/browser-playwright4.1.10^4.1.10
@vitest/coverage-v84.1.10^4.1.10
playwright1.64.0^1.64.0
@chromatic-com/storybook5.4.0^5.4.0

このとき、npm が配っている Vitest の最新版は 5.0.3 でしたが、入ったのは 4.1.10 でした。本ガイドのテストは、4.1.10 でも 5.0.3 でも、すべて通ることを確かめています。入った版のまま先へ進めて構いません。

Vitest 5 に上げる場合

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 の拡張子を外し、コメントを足しています。

.storybook/main.ts
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-docsstory からドキュメントのページを作ります本ガイドでは扱いません
@storybook/addon-a11yアクセシビリティを自動で検査しますこの章と 07 章
@storybook/addon-viteststory を 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 のようなふつうのディレクトリは名前だけで対象になるので、同じつもりで書くと見落とします。中のファイルまで届くパターンで書きます。

tsconfig.app.json
"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。

.storybook/preview.tsx
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 つを書きます。

src/components/notifications-dialog.stories.tsx
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 の型と照合されます
名前付きの export1 つが 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 の検査の対象になります

次に読む​

Footnotes​

  1. Install Storybook に、コマンドと、導入時に出る質問の説明があります。フラグの一覧と説明は npm create storybook@latest -- --help で確認できます。本文のフラグの意味は、この出力に基づいています。 ↩

  2. Storybook の公式のレシピ Integrate Tailwind CSS with Storybook も、preview のファイルで CSS を読み込む方法を示しています。 ↩

  3. Accessibility tests に、パネルの 3 つの分類、a11y.test の値、axe-core の検出率についての記載があります。3 つめの分類を、公式ドキュメントは Incomplete と書いています。Storybook 10.6 の画面の表示は Inconclusive なので、本文は画面に合わせました。 ↩