概要
tailwind-empty-state-ui で実装した EmptyState コンポーネントを Storybook に登録し、検索ゼロ・未登録・エラーの 3 バリアントを story として管理する。args / argTypes でプロパティをコントロールし、デザイナーや開発者がブラウザ上でバリエーションを確認できる環境を作る。
インストール
npx storybook@latest init
# または既存プロジェクトに追加
npm install --save-dev @storybook/nextjs @storybook/addon-essentials
実装
EmptyState コンポーネント(再掲)
// components/EmptyState.tsx
import type { ReactNode } from "react";
type Props = {
icon?: ReactNode;
title: string;
description?: string;
action?: ReactNode;
};
export default function EmptyState({ icon, title, description, action }: Props) {
return (
<div className="flex flex-col items-center justify-center px-6 py-16 text-center">
{icon && (
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-gray-100 text-gray-400">
{icon}
</div>
)}
<h3 className="mb-2 text-base font-semibold text-gray-800">{title}</h3>
{description && (
<p className="mb-6 max-w-sm text-sm leading-relaxed text-gray-500">{description}</p>
)}
{action && <div>{action}</div>}
</div>
);
}
EmptyState の story ファイル
// components/EmptyState.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import EmptyState from "./EmptyState";
// サンプル用 SVG アイコン
function SearchIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M21 21l-4.35-4.35M17 11A6 6 0 1 1 5 11a6 6 0 0 1 12 0z" />
</svg>
);
}
function InboxIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M20 13V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v7m16 0-2 6H6l-2-6m16 0H4" />
</svg>
);
}
function AlertIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 9v4m0 4h.01M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
</svg>
);
}
const meta: Meta<typeof EmptyState> = {
title: "UI/EmptyState",
component: EmptyState,
parameters: {
layout: "centered",
docs: {
description: {
component:
"リスト画面でデータが 0 件のときに表示する汎用コンポーネント。検索ゼロ・未登録・エラーの 3 シナリオに対応。",
},
},
},
tags: ["autodocs"],
argTypes: {
title: { control: "text" },
description: { control: "text" },
icon: { control: false },
action: { control: false },
},
};
export default meta;
type Story = StoryObj<typeof meta>;
// 検索結果ゼロ
export const SearchZeroResults: Story = {
name: "検索結果ゼロ",
args: {
icon: <SearchIcon />,
title: '"react hooks" に一致するアイテムはありません',
description: "キーワードを変えるか、フィルターを解除してみてください。",
action: (
<button className="rounded-lg border border-gray-300 px-5 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50">
検索をクリア
</button>
),
},
};
// データ未登録
export const NoData: Story = {
name: "データ未登録",
args: {
icon: <InboxIcon />,
title: "まだアイテムがありません",
description: "最初のアイテムを追加して始めましょう。",
action: (
<button className="rounded-lg bg-blue-600 px-5 py-2 text-sm font-medium text-white hover:bg-blue-700">
アイテムを追加
</button>
),
},
};
// エラー後
export const ErrorState: Story = {
name: "エラー後",
args: {
icon: <AlertIcon />,
title: "データの取得に失敗しました",
description: "通信エラーが発生しました。しばらく待ってから再試行してください。",
action: (
<button className="rounded-lg bg-red-600 px-5 py-2 text-sm font-medium text-white hover:bg-red-700">
再試行
</button>
),
},
};
// アイコンなし
export const WithoutIcon: Story = {
name: "アイコンなし",
args: {
title: "表示できる項目がありません",
description: "別の条件でお試しください。",
},
};
// 最小構成(title のみ)
export const TitleOnly: Story = {
name: "タイトルのみ",
args: {
title: "データがありません",
},
};
Storybook 設定(Tailwind CSS 対応)
// .storybook/main.ts
import type { StorybookConfig } from "@storybook/nextjs";
const config: StorybookConfig = {
stories: ["../components/**/*.stories.@(ts|tsx)"],
addons: [
"@storybook/addon-essentials",
"@storybook/addon-interactions",
],
framework: {
name: "@storybook/nextjs",
options: {},
},
};
export default config;
// .storybook/preview.ts
import type { Preview } from "@storybook/react";
import "../app/globals.css"; // Tailwind CSS を読み込む
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;
ポイント
tags: ["autodocs"]をmetaに付けると Storybook が自動でドキュメントページを生成する。argTypesで各 Prop の説明とコントロール種類を定義するとドキュメントが充実するiconとactionは JSX を受け取る Props のためcontrol: falseに設定し、Controls パネルには表示しない。Story ごとに具体的な値をargsで設定するparameters.layout: "centered"でコンポーネントを画面中央に配置して確認しやすくする- Story 名は
SearchZeroResultsのようにキャメルケースで定義し、nameで日本語の表示名を付ける。Storybook サイドバーに日本語で表示され、チーム内での共有がしやすくなる @storybook/nextjsを使うと Next.js のLink/Image/useRouterなどが story 内でも動作する。globals.cssをpreview.tsでインポートすると Tailwind CSS のスタイルが適用される