Next.js + Storybook
この文書では、Next.js のプロジェクトに Storybook を追加して利用する方法を紹介します。Storybook を利用すると、事前に用意したデータを利用してコンポーネントを表示することができるため、プログラムの開発と並行してコンポーネントの UI を作成していくことができます。
プロジェクトの作成
まず最初に、Next.js のプロジェクトを作成し、続いて Storybook のインストールをします。
Next.js のプロジェクトの作成
プロジェクトの作成として、Next.js + Tailwind CSS のプロジェクトを作成します。
npx create-next-app

注意**:**postcss.config.mjs に関しては Next.js のバージョンが上がり、下記の手順は不要になっています。インストールに進んでください。
なお、標準のテンプレートで表示される postcss.config.mjs の設定では Storybook のインストールの時にエラーが表示されます。作成時のファイルは以下のようになっています。
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;
これを以下のように書き換えてください。
const config = {
plugins: { "@tailwindcss/postcss": {} },
};
export default config;
上記の変更はインストールをそのまま実行するとエラーメッセージとして表示されます。あらかじめ変更しておきましょう。

Storybook のインストール
インストールの手順としては、Next.js の場合は公式サイトの React の手順で進めていきます。
以下のコマンドでインストールを進めます。
npm create storybook@latest
インストーラーが新しくインストールを実行するのかの確認を表示します。

しばらくすると、Storybook の画面が表示されます。

インストールが完了したターミナルの画面は以下の通りです。

ファイルの確認
Next.js に Storybook を追加した際の変更点を確認していきます。Visual Studio Code で変更点を見ると以下のようになります。

変更点だけをピックアップすると以下のようになります。
- .storybook storybook layout files
- src
- stories component resource
- package.json install storybook package
- vitest.config.ts config for Vitest
- vitest.shms.d.ts global type definitions for Vitest
インストール後の動作確認で npm run build を実行すると以下のようなエラーが表示されます。

これは、src/stories/Page.tsx のファイルの 43 行目で “args” という記述があり、これが ESlint でエラーになっています。以下のように書き換えてください。
<ul>
<li>
Use a higher-level connected component. Storybook helps you compose
such data from the "args" of child component stories
</li>
これで build の実行が可能となりました。
Storybook の動作確認
現在のプロジェクトでは、コマンドを実行することで Next.js と Storybook が共存している形です。まず、Next.js を起動するときは以下のコマンドです。
npm run dev
これで Next.js が http://localhost:3000 で起動します。
また Storybook を起動するときは以下のコマンドで起動します。
npm run storybook
これで Storybook が http://localhost:6006/ と Next.js とは別のポートで起動します。このため、同じプロジェクトで2つのプログラムが動くことがわかります。
なお、Storybook で作成したコンポーネントを build をして静的サイトとして展開することもできます。この場合は以下のコマンドで実行します。
npm run build-storybook
以下のように build が実行されます。

そしてサイトは storybook-static に展開しています。

Tailwind CSS を利用する
Storybook を設定した段階では、Next.js で利用している Tailwind CSS を参照していない状態です。そこで、Tailwind CSS を参照する形に Storybook の設定を変更していきます。
スタイルシートの整理
スタイルシートのファイルを整理していきます。
- 既存のプロジェクトでは Next.js の初期の状態となるため、src/app/globals.css に Tailwind CSS で利用するファイルが適用されています。これを、src/styles/globals.css に移動します。
- src/stories のフォルダの中に含まれる css ファイルを上記と同様に src/styles に移動します
- 移動したスタイルシートを、globals.css の import で読み込むようにします。
@import "tailwindcss";
@import "./button.css";
@import "./header.css";
@import "./page.css";
styles のフォルダの中は以下のようになります。
- src
- styles
- button.css
- globals.css
- header.css
- page.css
また、src/app/layout.tsx のファイルで読み込んでいるスタイルシートのパスも変更となるため、以下のように変更をします。
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "@/styles/globals.css";
スタイルシートの設定としては、storybook が参照できるようにするために、以下のファイルもスタイルシートを読み込むように変更をします。
import type { Preview } from "@storybook/nextjs-vite";
import "@/styles/globals.css";
const preview: Preview = {
parameters: {
これで、スタイルシートの調整は完了しました。
components フォルダの作成
Next.js で利用するコンポーネントとして、storybook で用意しているコンポーネントを利用できるように、以下のように変更をします。
- src/stories/Configure.mdx と src/stories/assets を削除します
- src/stories のフォルダを src/components のフォルダに変更します。
- src/components/Button.tsx 、src/components/Header.tsx および src/components/Page.tsx で読み込んでいる css の行を削除します
続いて上記のコンポーネントを Storybook が認識できるように変更を進めます。まず、.storybook/main.ts において、今回作成をした components のフォルダの配下にファイルを配置しているように設定を変更します。
import type { StorybookConfig } from "@storybook/nextjs-vite";
const config: StorybookConfig = {
stories: [
"../src/components/**/*.mdx",
"../src/components/**/*.stories.@(js|jsx|mjs|ts|tsx)",
],
上記の変更で、Next.js で作成をするコンポーネントに対して、stories.ts のファイルを定義して、コンポーネントの表示ができるようになりました。
コンポーネントの更新
今回の Next.js のプロジェクトは App Router としているため、Button.tsx および Page.tsx はクライアント側での処理が追加されているため、以下の1行を追加する必要があります。
"use client";
これで Next.js で利用できるコンポーネントになりました。
ただし、このコードを入れている場合 storybook で build を実行するとエラーが表示されます。以下のような画面が表示される形です。

前半の警告はまさに use client を追加したところが問題となっています。また後半の警告はサイズが大きくなっていることを示しています。
これを回避するために、.storybook/main.ts のファイルに以下のコードを追加します。
staticDirs: ["../public"],
viteFinal: async (config) => {
// plugin for remove "use client"
config.plugins = config.plugins || [];
config.plugins.push({
name: "remove-use-client",
transform(code, id) {
if (id.includes("src/components")) {
return code.replace(/['"]use client['"];?\s*/g, "");
}
},
});
config.build = config.build || {};
config.build.chunkSizeWarningLimit = 2000;
return config;
},
};
export default config;
これで警告が表示されない形となりました。
Storybook のファイル構成
最後に Storybook で定義している stories.tsx ファイルの役割を紹介します。まず、 Button.tsx のための定義ファイル src/components/Button.stories.ts を上から順に機能を紹介していきます。
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
import { fn } from 'storybook/test';
import { Button } from './Button';
上記のコードは、同じフォルダにある Button.tsx に対する stories.ts になっていることを定義しています。
const meta = {
title: 'Example/Button',
component: Button,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
layout: 'centered',
},
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
tags: ['autodocs'],
// More on argTypes: https://storybook.js.org/docs/api/argtypes
argTypes: {
backgroundColor: { control: 'color' },
},
// Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args
args: { onClick: fn() },
} satisfies Meta<typeof Button>;
export default meta;
これは Storybook の画面において表示される内容の定義になります。
- Example グループの Button として表示
- レイアウトで中央に表示
- autodocs を利用して、自動的に Doc ファイルを作成
- argTypes でコンポーネントで利用する変数の定義
- onClick の実装
コンポーネントで利用するデータの定義を設定する際には、argTypes の項目を増やすことで対応が可能です。最後の以下の項目で、表示をする場合の値を設定して、サンプルを表示できるようにしています。
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
primary: true,
label: 'Button',
},
};
export const Secondary: Story = {
args: {
label: 'Button',
},
};
export const Large: Story = {
args: {
size: 'large',
label: 'Button',
},
};
export const Small: Story = {
args: {
size: 'small',
label: 'Button',
},
};
ダークモードの追加
Storybook を利用することで、ライトモード、ダークモードの切り替えを画面上で変更可能となります。これを活用すると、コンポーネントの作成、確認の作業が非常にやりやすくなります。
Storybook の設定
まずは Storybook 側の設定を進めていきます。
npm install @storybook/addon-themes
続いて Storybook の設定ファイルにインストールをした Addon を追加します。
import type { StorybookConfig } from "@storybook/nextjs-vite";
const config: StorybookConfig = {
stories: [
"../src/components/**/*.mdx",
"../src/components/**/*.stories.@(js|jsx|mjs|ts|tsx)",
],
addons: [
"@chromatic-com/storybook",
"@storybook/addon-docs",
"@storybook/addon-onboarding",
"@storybook/addon-a11y",
"@storybook/addon-vitest",
"@storybook/addon-themes",
],
Storybook が表示する際に、Next.js と同じように Class を指定してモードの切り替えをするように、withThemeByClassName の関連コードを追加します。
import type { Preview } from "@storybook/nextjs-vite";
import { withThemeByClassName } from "@storybook/addon-themes";
import "@/styles/globals.css";
...
export const decorators = [
withThemeByClassName({
themes: {
light: "light",
dark: "dark",
},
defaultTheme: "light",
}),
];
export default preview;
Tailwind CSS の設定
この設定は簡単で、globals.css ファイルに以下の行を追加するだけでダークモードが動作するようになります。
@custom-variant dark (&:where(.dark, .dark *));
html {
@apply bg-white dark:bg-gray-900;
}
Card コンポーネントを追加する
ダークモードに対応した Card コンポーネントを作成していきます。まず、Card コンポーネントを以下のように作成します。
Card コンポーネントでは画像をアセットとして利用したいため、先にアセットの Interface を追加します。
export interface Asset {
_id: string;
alt: string;
url: string;
height?: number;
width?: number;
}
この Asset の定義を利用して、Card で扱うデータ項目を設定します。今回、Card の画像の位置に関しては top と left を用意しました。
import { Asset } from "@/types/asset";
import Image from "next/image";
import Link from "next/link";
export interface CardProps {
/** Title */
title: string;
/** Description */
description: string;
/** Option: Link URL */
href: string;
/** Image position type */
imagePosition?: "top" | "left";
/** Option: Image */
asset?: Asset;
}
export default function Card(props: CardProps) {
const { title, description, href, imagePosition, asset } = props;
const imagePositionClassName = imagePosition || "top";
return (
<div className={`component card ${imagePositionClassName}`}>
<Link href={href}>
{asset && (
<Image
src={asset.url}
alt={asset.alt}
width={asset.width}
height={asset.height}
/>
)}
<div className="p-4 flex-1">
<h3>{title}</h3>
<p>{description}</p>
</div>
</Link>
</div>
);
}
このカードのスタイルシートを以下のように作成しています。
.component.card {
@apply bg-white dark:text-white dark:bg-gray-700 rounded-2xl shadow-md overflow-hidden border dark:border-gray-900;
}
.component.card h3 {
@apply text-xl font-bold mb-2;
}
.component.card p {
@apply text-gray-700 dark:text-gray-300;
}
.component.card.left a {
@apply flex items-center;
}
.component.card.top a img {
@apply w-full h-48 object-cover;
}
.component.card.left a img {
@apply w-48 h-32 object-cover flex-shrink-0;
}
また Storybook のプレビューで確認するときにダークモードが有効になるように、以下のスタイルシートも作成をします。
.docs-story {
@apply bg-white dark:bg-gray-900;
}
両方のファイルを globals.css で読み込んでください。今回はサンプルのスタイルも削除しました。
@import "tailwindcss";
@import "./button.css";
@import "./header.css";
@import "./page.css";
@import "./card.css";
@import "./storybook.css";
@custom-variant dark (&:where(.dark, .dark *));
html {
@apply bg-white dark:bg-gray-900;
}
最後に、コンポーネントのサンプルを表示するファイルを以下のように作成をします。
// components/Card.stories.ts
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
import Card from "./Card";
const meta: Meta<typeof Card> = {
title: "Components/Card",
component: Card,
tags: ["autodocs"],
};
export default meta;
type Story = StoryObj<typeof Card>;
export const Default: Story = {
args: {
title: "Sample Card",
description: "This is a description for the sample card component.",
href: "/sample-card",
asset: {
_id: "sample-image",
alt: "Sample Image",
url: "/ludemeula-fernandes-9UUoGaaHtNE-unsplash.jpg",
height: 1920,
width: 1280,
},
},
};
export const ImageLeft: Story = {
args: {
title: "Sample Card with Image on Left",
description: "This is a description for the sample card component.",
href: "/sample-card",
asset: {
_id: "sample-image",
alt: "Sample Image",
url: "/ludemeula-fernandes-9UUoGaaHtNE-unsplash.jpg",
height: 1920,
width: 1280,
},
imagePosition: "left",
},
};
export const NoImage: Story = {
args: {
title: "Card Without Image",
description: "This card has no image but still renders nicely.",
href: "/sample-card",
},
};
なお、public に画像をコピーして、その画像ファイルを参照するように実装しています。
動作確認
それでは実際の動作確認を進めます。以下のコマンドを実行します。
npm run storybook
Storybook が起動すると、今回追加をしたボタンが左側のメニューに追加されています。選択をすると以下のような画面になります。

モードの切替ボタンをクリックして、ダークモードにします。

意図した形でダークモードでの色が設定できています。続いて、画像の位置を左側にした場合の動作を確認すると、以下のような形で左側の時の表示も確認できました。

これでダークモードでの動作、そして表示に関するテストができました。
Vercel に展開する
今回作成をした Storybook のデータをいつでもサイトで参照できるように、Vercel に展開していきます。
- Vercel で New Project を開く
- 該当するリポジトリを指定する
- Framework Preset を Storybook を選択する

環境変数などはないため、追加の設定なしに展開をすることが可能です。しばらくすると、以下のように参照できるようになりました。

まとめ
今回は Next.js App Router に対して Storybook をインストール、そして Tailwind CSS を共有するところまで進めていきました。今回実施したサンプルのコードは以下のリポジトリ、およびデモサイトを参照できるようにしてあります。
- GitHub Repository - https://github.com/haramizu/nextjs-storybook
- Demo site - https://haramizu-nextjs-storybook-demo.vercel.app/