StorybookはUIコンポーネントをアプリケーションから独立した状態で開発を行うためのツールです。propsを通してさまざまなバリエーションのデザインをブラウザ上に静的に表示させることがでできます。さらにブラウザ上からpropsの操作、外部リソースからのデータ取得、またテストライブラリーを利用して関数の実行、テストを行うことも可能です。またStorybookに登録したコンポーネントは自動でドキュメント化することができるのでドキュメントツールとして利用することもできます。

StorybookはReactに限らず、Vue.js, React Native, Svelte, Amber, AngularなどさまざまなJavaScriptフレームワークに対応しています。本文書ではVite上に構築したReact+TypeScript環境にStrorybookをインストールします。シンプルなコードを利用して動作確認を行なっているのでStorybookの基礎的な利用方法や機能を短時間で理解することができます。

Storybookの最新バージョンは7を利用して動作確認を行っていますがバージョン6でのReact+JavaScript環境の記事はすでに公開済みです。

環境の構築

Reactプロジェクトの作成

Viteを利用してReactのプロジェクトの作成を行います。npm create vite@latestコマンドを実行するとプロジェクト名とフレームワークの選択とvariantでTypeScriptを利用するか聞かれるのでフレークワークはReact, TypeScriptを選択してください。プロジェクト名は任意なのでここではreact-storybook7に設定しています。


 % npm create vite@latest                                  
✔ Project name: … react-storybook7
✔ Select a framework: › React
✔ Select a variant: › TypeScript

Scaffolding project in /Users/mac/Desktop/react-storybook7...

Done. Now run:

  cd react-storybook7
  npm install
  npm run dev

作成されるプロジェクトフォルダreact-storybook7に移動してnpm installコマンドを実行します。

Storybookのインストール

npx storybook@latest initでstorybookのインストールを行います。


 % npx storybook@latest init
Need to install the following packages:
  storybook@7.0.17
Ok to proceed? (y) y

 storybook init - the simplest way to add a Storybook to your project. 

 • Detecting project type. ✓
 • Detected Vite project. Setting builder to Vite. ✓
 • Adding Storybook support to your "React" app
  ✔ Getting the correct version of 10 packages
✔ We have detected that you're using ESLint. Storybook provides a plugin that gives the best experience with Storybook and helps follow best practices: https://github.com/storybookjs/eslint-plugin-storybook#readme

Would you like to install it? … yes
    Configuring Storybook ESLint plugin at .eslintrc.cjs
  ✔ Installing Storybook dependencies
. ✓
 • Preparing to install dependencies. ✓



up to date, audited 1043 packages in 2s

181 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
. ✓

For more information visit: https://storybook.js.org

To run your Storybook, type:

   npm run storybook 

storybookをインストール後のpackage.jsonを確認しておきます。


{
  "name": "react-storybook7",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview",
    "storybook": "storybook dev -p 6006",
    "build-storybook": "storybook build"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@storybook/addon-essentials": "^7.0.17",
    "@storybook/addon-interactions": "^7.0.17",
    "@storybook/addon-links": "^7.0.17",
    "@storybook/blocks": "^7.0.17",
    "@storybook/react": "^7.0.17",
    "@storybook/react-vite": "^7.0.17",
    "@storybook/testing-library": "^0.0.14-next.2",
    "@types/react": "^18.0.28",
    "@types/react-dom": "^18.0.11",
    "@typescript-eslint/eslint-plugin": "^5.57.1",
    "@typescript-eslint/parser": "^5.57.1",
    "@vitejs/plugin-react": "^4.0.0",
    "eslint": "^8.38.0",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-react-refresh": "^0.3.4",
    "eslint-plugin-storybook": "^0.6.12",
    "prop-types": "^15.8.1",
    "storybook": "^7.0.17",
    "typescript": "^5.0.2",
    "vite": "^4.3.2"
  }
}

npm run storybookコマンドを実行するとブラウザが自動で起動してWelcomeページが表示されます。

Storybookの初期画面
Storybookの初期画面

サンプルデータの確認

サイドメニューにあるButtonを展開するとDocs, Primay, Secondary, Large, Smallが表示されDocsをクリックするとButtonコンポーネントに関するドキュメント化された情報が表示されます。ButtonコンポーネントのデザインだけではなくPropsの情報も表示されます。

Docsの内容
Docsの内容

サイドメニューのDocs以外をクリックしてみましょう。Primaryだとブルーカラーのボタン、Secondaryだと白いボタン、Largeは大きなボタン、Smallだと小さなボタンというように同じボタンでも異なるデザインのボタンが表示されます。コンポーネントが持つpropsを利用して1つのコンポーネントから作成した複数のデザインをブラウザ上で確認することができます。Storybookではコンポーネントが表示されている部分をCanvasといいます。

Buttonを確認
PrimaryのButtonを確認

サイドメニューに表示されているButton, Header, Pageなどはコンポーネントに対応する名前です。Buttonの下の階層にあるPrimary, Seccondary,…などをStorybookではストーリーと呼びます。1つのコンポーネントに対して複数のストーリーを設定すると各ストーリーで設定した値を元にCanvasにコンポーネントが描写されます。

フォルダ構成の確認

サイドメニューに表示されているストーリーがどのファイルに記述されているか確認するためにstorybookインストール後のフォルダ構成を確認します。プロジェクトフォルダ(react-storybook7)の中にstorybookに関係する2つのフォルダを確認することができます。1つはプロジェクトフォルダ直下に作成される.storybookフォルダ、もう一つはsrcフォルダ直下に作成されるstoriesフォルダです。

ブラウザ上に表示されている内容を記述したファイルはstoriesフォルダの中に保存されているので中身を確認するとメニューに表示されているIntroduction, Button, Header, Pageに対応するファイルを確認することができます。それらのファイルの中でファイル名に.stories.tsが含まれているファイルがStorybookのストーリーを記述しているファイルです。

初めてのStoryBook設定

サンプルとして作成されているstoriesフォルダの下にあるファイルを確認することもできますが初めてStorybookを利用する人にとって少しハードルが高いように思われるのでstoriesフォルダの中身はスキップして別のフォルダで一からストーリーを設定していきます。

Buttonコンポーネントの設定

Buttonコンポーネントを利用してStorybookの動作確認をしたいのでsrcフォルダの下にcomponentsフォルダ、componentsの下にButtonフォルダを作成しその下にButton.tsxファイルとButton.stories.tsファイルを作成します。

Button.tsxファイルとButton.stories.tsファイルは同じフォルダに作成する必要はありません。srcフォルダの下であればフォルダ構成は自由に決めることができます。componentsフォルダの外側にButton.stories.tsファイルを作成することも可能です。

Button.tsxファイルに下記の内容を記述します。親コンポーネントからpropsでchildrenを受け取るだけのシンプルなコンポーネントです。


type Props = {
  children: React.ReactNode;
};

function Button({ children }: Props) {
  return <button>{children}</button>;
}

export default Button;

ButtonコンポーネントのストーリーをButton.stories.tsファイルに記述していきます。ストーリーといっても何か特別なものではなくButtonコンポーネントをブラウザ上でどのように描写するのかを決めるJavaScriptの関数です。ストーリーはComponent Story Format(CSF)というフォーマットを利用して記述していきます。

export defaultの中にmetadataであるtitleとcomponentを設定します。titleはサイドメニューのストーリーをまとめていたButton, Header, Pageに対応します。componentはストーリーを設定するコンポーネントを指定します。コンポーネントはimportしておく必要があります。


import type { Meta } from '@storybook/react';

import Button from './Button';

const meta = {
  title: 'Button',
  component: Button,
} satisfies Meta<typeof Button>;

export default meta;

上記の設定の結果、exportしているmetaの型を確認すると以下のようになっています。


const meta: {
    title: string;
    component: ({ children }: Props) => JSX.Element;
}

次にストーリーを追加します。ストーリーにHelloButtonという名前をつけてargs(argments)でButtonコンポーネントで定義したprops childernに対して”Hello World”という文字列を渡しています。定義したHelloButton関数はStroybookで利用するためにexportする必要があります。


import type { Meta, StoryObj } from '@storybook/react';

import Button from './Button';

const meta = {
  title: 'Button',
  component: Button,
} satisfies Meta<typeof Button>

export default meta;

type Story = StoryObj<typeof Button>

export const HelloButton: Story = {
  args: {
    children: 'Hello World',
  },
};

設定は完了したのでブラウザで確認すると新たにサイドメニューに設定した”Hello Button”がCanvas上に表示されます。

追加したボタンの表示
追加したボタンの表示

ストーリーはrender関数を利用して記述することもできます。render関数の場合ファイルの中でJSXを記述するので拡張子をtsxに変更する必要があります。tsのままだとエラーになります。


import type { Meta, StoryObj } from '@storybook/react';

import Button from './Button';

const meta = {
  title: 'Button',
  component: Button,
} satisfies Meta<typeof Button>;

export default meta;

type Story = StoryObj<typeof Button>;

export const HelloButton: Story = {
  render: () => <Button>Hello World</Button>,
};

render関数を利用しても先ほどと同様に”ボタン”Hello Button”が表示されます。

表示するストーリーの制御

現在の設定ではsrcフォルダ下に存在するstories.ts(tsx)ファイルが自動で検知されるように設定されているので.storybookフォルダに存在する設定ファイルmain.tsの内容を変更します。

設定変更は必須ではありませんが混乱しないためにゼロから作成したストーリーのみブラウザ上のStorybookに表示させるため設定を行なっています。インストール直後から存在するstoriesフォルダのストーリーが表示されたままで問題ない場合はそのまま進めてください。

componentsフォルダ下でstories.js/jsx/ts/tsxという名前が入ったファイルのみ自動検知できるように変更しています。


import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
  // stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
  stories: [
    '../src/components/**/*.mdx',
    '../src/components/**/*.stories.@(js|jsx|ts|tsx)',
  ],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
  ],
  framework: {
    name: '@storybook/react-vite',
    options: {},
  },
  docs: {
    autodocs: 'tag',
  },
};
export default config;

設定ファイルのmain.tsを更新した場合はStorybookを再起動する必要があるのでnpm run storybookを再実行してください。Buttonのみ表示されるようになりました。

componentsフォルダ下のStoryのみ表示
componentsフォルダ下のStoryのみ表示

ストーリーの追加

Button.stories.tsファイルの中のmetadataのtitleに設定したButtonはサイドメニューに表示されその中にストーリーHello Buttonが表示されます。ストーリーHello Buttonを選択すると”Hello World”のテキストが入ったボタンが表示されます。

ボタンに表示される文字を変えたい場合には同じファイル(button.stories.ts)の中に新たにストーリーを追加することでテキストの変更によってデザインにどのような違いができるのかを確認することができます。HelloButtonはrender関数、ClickButtonはargsを利用してストーリーを設定していますが表示に影響はありません。


import type { Meta, StoryObj } from '@storybook/react';

import Button from './Button';

const meta = {
  title: 'Button',
  component: Button,
} satisfies Meta<typeof Button>;

export default meta;

type Story = StoryObj<typeof Button>;

export const HelloButton: Story = {
  render: () => <Button>Hello World</Button>,
};

export const ClickButton: Story = {
  args: {
    children: 'click',
  },
};

追加後にブラウザを確認するとClickButtonのストーリーが左のサイドメニューに自動で追加され、ClickButtonを選択するとCanvas上のボタンには設定したテキストClickが表示されます。

追加したClickButtonの確認
追加したClickButtonの確認

titleの変更

titleを変更することでサイドメニューに表示される名前を変更することができますが階層化することも可能です。例えばtitleをButtonからCommon/Buttonに変更します。


const meta = {
  title: 'Common/Button',
  component: Button,
} satisfies Meta<typeof Button>

Buttonの上にCOMMONが表示され階層化されることが確認できます。

階層化したStory
階層化したStory

titleを設定しない場合はStrobookが自動で設定を行なってくれます。metaのcomponentをコメントすると現在の設定ではHelloButtonは表示されますがClickButtonはエラーになり表示されます。ClickButtonもrender関数でButtonタグを利用することでエラーは解消します。

.storybookフォルダの確認

Storybookをインストール時に作成される.stroybookフォルダにはStrorybookの設定ファイルが保存されています。メインの設定ファイルであるmain.tsファイルを確認するとstoriesプロパティにはstoryファイルのパスが配列で設定されています。この設定によりsrcフォルダ 下の*.stories.tsx/tsが自動で認識されることを先ほど確認しました。addonsプロパティにはStorybookのアドオンが設定されておりStorybookの機能を拡張することができます。その他の設定できる項目についてはStorybookのドキュメントから確認することができます。


import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
  // stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
  stories: [
    '../src/components/**/*.mdx',
    '../src/components/**/*.stories.@(js|jsx|ts|tsx)',
  ],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
  ],
  framework: {
    name: '@storybook/react-vite',
    options: {},
  },
};
export default config;

addonsの確認

addonsによりStorybookにどのような機能拡張が行われるのか確認していきます。

デフォルトの状態のブラウザ上に表示されるStorybookの画面を見ると上部にはさまざまなアイコンが表示されていることが確認できます。この部分をToolbarと呼びます。

追加したClickButtonの確認
アイコンの確認

main.tsファイルを開いてaddonsの中からaddon-essentialsをコメントします。


import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
  // stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
  stories: [
    '../src/components/**/*.mdx',
    '../src/components/**/*.stories.@(js|jsx|ts|tsx)',
  ],
  addons: [
    '@storybook/addon-links',
    // '@storybook/addon-essentials',
    '@storybook/addon-interactions',
  ],
  framework: {
    name: '@storybook/react-vite',
    options: {},
  },
  docs: {
    autodocs: 'tag',
  },
};
export default config;

main.tsを更新した後は設定を反映させるためnpm run storybookを再実行する必要があります。

画面を確認するとToolbarに表示されていたアイコンが5つ消えていることがわかります。

addon-essentialsを無効にした場合
addon-essentialsを無効にした場合

addonsのessentialは複数のaddonsが含まれており公式ドキュメントから確認することができます。ドキュメントからDocs, Controls, Actions, Viewport, Backgrounds, Toolbars&globals, Measure&outlineが含まれていることがわかります。このことからToolbar上から消えたアイコンはessentialに含まれるaddonsということがわかります。

ここではそれぞれの機能がどのような動作を行うのか確認していませんがaddonsによってStorybookの機能拡張が行われていることを理解することができました。再度main.tsを元の状態に戻してaddonsのessentialsを利用できる状態にしてください。main.tsファイルの更新を行ったらnpm run storybookの再実行も忘れずに行なってください。

Autodocs

設定したStoryのコンポーネントに対してのドキュメントを自動作成してくれる機能がAutodocsです。stories.tsxファイルにtagsを追加することで自動で作成が行われます。


import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
  // stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
  stories: [
    '../src/components/**/*.mdx',
    '../src/components/**/*.stories.@(js|jsx|ts|tsx)',
  ],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
  ],
  framework: {
    name: '@storybook/react-vite',
    options: {},
  },
  docs: {
    autodocs: 'tag',
  },
};
export default config;

npm run storybookの再実行を行いブラウザを確認するとButtonの下にDocsが追加されていることがわかります。Storyも表示され一括でコンポーネントのデザインを確認することができます。

表示されたDocsの確認
表示されたDocsの確認

ストーリーの追加/更新

スタイルの適用

ButtonコンポーネントにCSSによるスタイルが適用できるようにButtonフォルダの中にbutton.cssファイルを作成してbutton要素に対してスタイルを適用します。

用します。


button {
  font-size: 14px;
  padding: 10px;
  border: 0;
  border-radius: 1em;
  cursor: pointer;
  display: inline-block;
}

作成したbutton.cssファイルをButton.tsxファイルでimportします。


import './button.css';

type Props = {
  children: React.ReactNode;
};

function Button({ children }: Props) {
  return <button>{children}</button>;
}

export default Button;

スタイルが適用されブラウザ上のボタンが変更されます。

スタイル適用後のボタン
スタイル適用後のボタン

propsの値によって背景色を変更できるように新たにpropsにcolorを追加します。デフォルト値をdefaultとします。


import './button.css';

type Props = {
  children: React.ReactNode;
  color?: string;
};

function Button({ children, color = 'default' }: Props) {
  return <button className={color}>{children}</button>;
}

export default Button;

defaultのclassをbutton.cssに追加します。button要素の文字の色をwhiteに設定します。


button {
  font-size: 14px;
  padding: 10px;
  border: 0;
  border-radius: 1em;
  cursor: pointer;
  display: inline-block;
  color: white;
}

.default {
  background-color: #6c757d;
}

デフォルトの背景色と文字が白になっていることが確認できます。

propsを設定したボタンのデフォルト値
propsを設定したボタンのデフォルト値

propsのcolorの値によってボタンのデザインが変わるようにbutton.stories.tsxで設定したストーリーを変更します。ストーリーの名前をDefault, Primaryに設定してPrimaryではpropsでcolorの値をprimaryとしています。


import type { Meta, StoryObj } from '@storybook/react';

import Button from './Button';

const meta = {
  title: 'Button',
  component: Button,
  tags: ['autodocs'],
} satisfies Meta<typeof Button>;

export default meta;

type Story = StoryObj<typeof Button>;

export const Default: Story = {
  args: {
    children: 'Default',
  },
};

export const Primay: Story = {
  args: {
    children: 'Primary',
    color: 'primary',
  },
};

button.cssファイルにprimaryクラスを追加します。


.primary {
  background-color: #007bff;
}

Primaryのストーリーを選択すると背景色がブルーのボタンが表示されます。

propsを利用して背景色を設定
propsを利用して背景色を設定

さらに背景色の異なるストーリーDangerを追加することもできます。


export const Danger: Story = {
  args: {
    children: 'Danger',
    color: 'danger',
  },
};

CSSの追加も合わせて行います。


//略
.danger {
  background-color: #dc3545;
}

新たにDangerのストーリーが追加されます。

Dangerボタンの確認
Dangerボタンの確認

Buttonコンポーネントに設定したcolor propsに対応したストーリーを作成し、それぞれのストーリーでcolor propsに渡す値を変えることで渡した値によってどのようにボタンが表示されるかブラウザ上で確認できるようになりました。

サイズの変更

Buttonコンポーネントに背景色だけではなくサイズの変更もできるようにpropsにsizeを追加します。


import './button.css';

type Props = {
  children: React.ReactNode;
  color?: string;
  size?: string;
};

function Button({ children, color = 'default', size = 'base' }: Props) {
  return <button className={`${color} ${size}`}<{children}</button>;
}

export default Button;

size propsのdefault値はbaseとしています。baseの他にsm, lgも設定できるようにsmとlgもbutton.cssファイルに追加しておきます。文字の大きさだけではなくpaddingも変更するためbutton要素に設定していたpaddingをbase, sm, lgで設定できるように変更します。


button {
  border: 0;
  border-radius: 1em;
  cursor: pointer;
  display: inline-block;
  color: white;
}

//略

.base {
  font-size: 14px;
  padding: 10px;
}

.sm {
  font-size: 12px;
  padding: 8px;
}

.lg {
  font-size: 18px;
  padding: 14px;
}

Button.stories.tsxにカラーとサイズを指定したストーリー(PrimarySmall, PrimaryLarge)を追加します。


export const PrimarySmall: Story = {
  args: {
    children: 'Primary',
    color: 'primary',
    size: 'sm',
  },
};

export const PrimaryLarge: Story = {
  args: {
    children: 'Primary',
    color: 'primary',
    size: 'lg',
  },
};

設定が完了するとブラウザ上から”Primary Large”と”Primary Small”が選択できるようになります。Primary Largeの場合は大きなボタン、Primary Smallの場合は小さなボタンが表示されます。Primaryの場合は通常のサイズのボタンが表示されます。

大きなサイズのボタン
大きなサイズのボタン

propsがある場合もrender関数を利用して記述することはできます。


export const PrimarySmall: Story = {
  render: () => (
    <Button color="primary" size="sm">
      Primary
    </Button>
  ),
};

export const PrimaryLarge: Story = {
  render: () => (
    <Button color="primary" size="lg">
      Primary
    </Button>
  ),
};

Argsの再利用

argsを利用している場合は別のストーリーのargsを再利用することができます。

PrimaryとPrimarySmall, PrimaryLargeの3つのargsを比較した時children, colorは共通でsizeだけ異なる値が設定されていることがわかります。その場合はPrimarのargsを…Primary.argsとして別のストーリーで利用することができます。


export const Primary: Story = {
  args: {
    children: 'Primary',
    color: 'primary',
  },
};

//略

export const PrimarySmall: Story = {
  args: {
    ...Primary.args,
    size: 'sm',
  },
};

export const PrimaryLarge: Story = {
  args: {
    ...Primary.args,
    size: 'lg',
  },
};

Args+render関数

argsとrender関数を同時に利用することができます。その場合はrender関数の引数でargsを受け取ることができます。


export const Danger: Story = {
  render: (args) => <Button {...args}>{args.children}</Button>,
  args: {
    children: 'Danger',
    color: 'danger',
  },
};

Storybookが持つ機能の確認

Controls

Controlsの動作確認

StorybookのControlsを利用することでコード上ではなくUI上でargsの値を変更することができます。ControlsはAddonの一つなので@storybook/addon-essentialsの中に含まれています。

下記のようにDangerを設定したStoryをブラウザ上で確認します。


export const Danger: Story = {
  args: {
    children: 'Danger',
    color: 'danger',
  },
};

Dangerボタンの下部にControls, Actions, Interactionsのタブを確認することができます。

Controlsの確認
Controlsの確認

Controlsは非表示にすることもできるので表示されない場合はまずブラウザの右上にあるアイコンを確認します。下記の場合は左から4番目にあるアイコンがControlsのアイコンでこれが表示されている場合はブラウザ上にはControlsは表示されていません。クリックすると下部に表示されます。Controlsの表示はブラウザの右側にも表示させることも可能です。アイコンが表示されていないにも関わらず下部、右側にも表示されていない場合はブラウザのデベロッパーツールのApplicationのlocalStorageを一度クリアにしてみてください。

Controlsのアイコンの確認
Controlsのアイコンの確認

Buttonコンポーネントはchildren, color, sizeのpropsを持っているのでControlsにはpropsの名前とControl列が表示されています。UI上でargsの値を変更できると説明した通り、childrenのControl列のDangerをエラーに変更してみます。

UI上のボタンの文字がDangerからエラーに更新されます。argsの値をUI上で変更できることがわかりました。

childrenの値を更新
childrenの値を更新

DangerのStoryをrender関数を変更して表示させます。


export const Danger: Story = {
  render: () => <Button color="danger">Danger</Button>,
};

render関数の場合はControls上でargsの値を変更することはできません。”This story is not configured to handle controls”と表示されています。

render関数で設定した場合
render関数で設定した場合

propsの型の設定

Controlsの機能を利用するためにrender関数をargsの形に戻します。


export const Danger: Story = {
  args: {
    children: 'Danger',
    color: 'danger',
  },
};

再度Controlsを確認するとchildren, color, sizeすべての値は自由な値を設定することができます。

Controlsの確認
Controlsの確認

childrenに関しては問題がありませんがcolorについてはdefault, primary, dangerのみCSSファイルに設定しています。TypeScriptでpropsの型にUnionを利用することで設定可能な値を制限することができます。Button.tsxファイルでcolorとsizeの型を更新します。


import './button.css';

type Props = {
  children: React.ReactNode;
  color?: 'default' | 'primary' | 'danger';
  // color?: string;
  size?: 'base' | 'sm' | 'lg';
  // size?: string;
};

function Button({ children, color = 'default', size = 'base' }: Props) {
  return <button className={`${color} ${size}`}>{children}</button>;
}

export default Button;

Controlsを確認するとcolor, sizeがradioボタンで選択できる値が制限されていることがわかります。

Controlsの設定
Controlsの設定
コンポーネントファイルを更新後、Controlsに設定した値が反映されない場合はStorybookの再起動を行なってください。

colorやsizeにButton.tsxのUnionの設定以外の値を設定するとButton.stories.tsxファイルでTypeScriptのエラーが発生します。

argTypesの設定

Button.tsxファイルのpropsの型を設定することでControlsのcolor, sizeの設定する値を制限することができました。コンポーネントファイルではなくストーリーファイルでも値の制限を行うことができます。

動作確認のためpropsの型をstringに戻します。


type Props = {
  children: React.ReactNode;
  // color?: 'default' | 'primary' | 'danger';
  color?: string;
  // size?: 'base' | 'sm' | 'lg';
  size?: string;
};

Button.strories.tsxではargTypesを利用してcolor, sizeの設定を行います。optionsでは設定できる値、controlのtypeでradioとselectを設定します。


const meta = {
  title: 'Button',
  component: Button,
  tags: ['autodocs'],
  argTypes: {
    color: {
      options: ['primary', 'default', 'danger'],
      control: { type: 'radio' },
    },
    size: {
      options: ['sm', 'base', 'lg'],
      control: { type: 'select' },
    },
  },
} satisfies Meta<typeof Button>;

typeにradioを設定するとラジオボタン、selectに設定するとselect要素として選択することができます。

argTypesの設定後のControls
argTypesの設定後のControls

Button.tsxでUnion Typeを設定してButton.stores.tsxのargTypesのoptionsを設定した場合、Storybook上ではargTypesで設定した値が表示されます。

argTypesでoptionsを設定せずcontrolのtypeのみ設定するとcontrolのtypeのみ反映されます。

Actions

clickイベントとActions

actionsの動作確認を行うためにButtonコンポーネントにonClickイベントを設定します。


import './button.css';

type Props = {
  children: React.ReactNode;
  color?: 'default' | 'primary' | 'danger';
  size?: 'base' | 'sm' | 'lg';
  onClick?: () => void;
};

function Button({
  children,
  color = 'default',
  size = 'base',
  onClick = () => console.log('click'),
}: Props) {
  return (
    <button className={`${color} ${size}`} onClick={onClick}>
      {children}
    </button>
  );
}

export default Button;

ブラウザ上に表示されているボタンをクリックしてください。Controlsの横のActionsにクリックの回数とクリックイベントの情報が表示されます。

Actionsの表示
Actionsの表示

ButtonコンポーネントにonClickイベントを追加するだけでActionsが動作するようになりました。

argTypesの設定

Buttonコンポーネントではpropsで渡す名前をonClickとしていましたがhandleClickに変更します。名前をonClickからhandleClickに変更するとActionsは動作しなくなります。

handleClickでもActionsを動作させるためにButton.stories.tsxファイルのargTypesを設定します。


const meta = {
  title: 'Button',
  component: Button,
  tags: ['autodocs'],
  argTypes: {
    handleClick: {
      action: 'clicked',
    },
  },
} satisfies Meta<typeof Button>;

設定を行いボタンをクリックするとActionsにクリックのイベント情報が表示されます。argTypesのactionプロパティで設定して’clicked’は下記のようにメッセージの先頭に表示されます。

argTypesの設定によるActionsの動作確認
argTypesの設定によるActionsの動作確認

グローバル設定

なぜonClickの名前ではActionsが動作したのにhandleClickの名前ではargTypesを設定する必要があるのしょう。理由は.storybookフォルダの下にあるpreview.tsファイルで設定が行われているためです。


import type { Preview } from "@storybook/react";

const preview: Preview = {
  parameters: {
    actions: { argTypesRegex: "^on[A-Z].*" },
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/,
      },
    },
  },
};

export default preview;

parametersのactionsのargTypesRegexにonから始まる名前のみActionsが動作するように設定が行われています。

parametersの設定はButton.stories.tsxファイルでも設定することができるので下記のように設定を行います。


const meta = {
  title: 'Button',
  component: Button,
  tags: ['autodocs'],
  parameters: { actions: { argTypesRegex: '^handle[A-Z].*' } },
} satisfies Meta<typeof Button>;

先頭がhandleから始まる関数であればActionsが動作します。

parameterの設定によるActionsの動作確認
parameterの設定によるActionsの動作確認

Loadersによるデータ取得

外部サービスから非同期に取得したデータをLoadersによって利用することもできます。ここでは無料で利用できる外部サービスのJSONPladeHolderを利用します。

UserItemコンポーネントを作成します。idとnameを受け取れるようにpropsを設定を行っています。


type Props = {
  id: string;
  name: string;
};

const UserItem = ({ id, name }: Props) => {
  return (
    <li>
      {id}:{name}
    </li>
  );
};

export default UserItem;

UserItem.stories.tsxファイルを作成して以下のコードを記述します。


import type { Meta, StoryObj } from '@storybook/react';

import UserItem from './UserItem';

const meta = {
  title: 'UserItem',
  component: UserItem,
  tags: ['autodocs'],
} satisfies Meta<typeof UserItem>;

export default meta;

type Story = StoryObj<typeof UserItem>;

export const Default: Story = {
  args: {
    id: '1',
    name: 'John Doe',
  },
};

Storybookで確認するとUserItemがサイドバーに追加されます。

Stroybook上で表示されたUserListコンポーネント
Stroybook上で表示されたUserListコンポーネント

Loadersを利用してJSONPlaceHolderからデータを取得するためにfetch関数を利用します。Node.js環境でfetch関数を利用するためにはNodeのバージョンが18以上である必要があります。本環境はv18.15で動作確認しています。

FetchDataという名前でストーリーを追加しています。


export const FetchData: Story = {
  loaders: [
    async () => ({
      user: await (
        await fetch('https://jsonplaceholder.typicode.com/users/1')
      ).json(),
    }),
  ],
  render: (args, { loaded: { user } }) => (
    <UserItem {...args} id={user.id} name={user.name} />
  ),
};

Storybookで確認するとJSONPladeHolderから取得したデータが”Fetch Data”で表示されています。

loadersを利用して取得したデータを表示
loadersを利用して取得したデータを表示

loadersを利用して取得したデータをストーリーで利用できることがわかりました。

loadersの設定をmetaの中で行うこともできます。その場合はすべてのストーリーでloadersを利用することができます。


import type { Meta, StoryObj } from '@storybook/react';

import UserItem from './UserItem';

const meta = {
  title: 'UserItem',
  component: UserItem,
  tags: ['autodocs'],
  loaders: [
    async () => ({
      user: await (
        await fetch('https://jsonplaceholder.typicode.com/users/2')
      ).json(),
    }),
  ],
} satisfies Meta<typeof UserItem>;

export default meta;

type Story = StoryObj<typeof UserItem>;

export const FetchData: Story = {
  render: (args, { loaded: { user } }) => (
    <UserItem {...args} id={user.id} name={user.name} />
  ),
};

export const Default: Story = {
  render: (args, { loaded: { user } }) => (
    <UserItem id={args.id} name={user.name} />
  ),
  args: {
    id: '100',
    name: 'John Doe',
  },
};

Play Functionの設定

コンポーネントに定義した関数をStorybookで描写する前に実行することができます。例えばボタンコンポーネントであればPlay Functionを利用してボタンをクリックしてユーザとのインタラクションの動作確認を行うことができます。

Play Functionを利用するためにはaddOnの@storybook/addon-interactionsをインストールする必要がありますがデフォルトでインストールが完了しています。@storybook/testing-libraryも利用しますがこちらもデフォルトでインストール済みです。追加のインストールなしで利用することができます。

Buttonコンポーネントではボタンをクリックすると実行されるhandleClickはconsole.logが実行されるようになっていますがalert関数が実行されるようにargsを設定します。


export const Danger: Story = {
  args: {
    children: 'Danger',
    color: 'danger',
    handleClick: () => alert('click'),
  },
};

alert関数を設定するとボタンをクリックするとポップアップが表示されます。

ボタンをクリックするとポップアップ表示
ボタンをクリックするとポップアップ表示

Play Functionを追加します。play Functionのコードの中身については後ほど説明します。


export const Danger: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    const button = await canvas.getByRole('button');
    await userEvent.click(button);
  },
  args: {
    children: 'Danger',
    color: 'danger',
    handleClick: () => alert('click'),
  },
};

追加後にDangerのストーリーを選択すると自動でポップアップが表示されます。Interationsタグに数字が1と表示され”PASS”と表示されています。動作確認(テスト)に成功したことを意味します。”Go Back”, “Go Forwad”ボタンにより何度もPlay Functionを実行することができます。

Play Functionの実行
Play Functionの実行

PlayFunctionの中で利用されていたcanvasElementやcanvas, buttonにはどのような情報が入っているのかconsole.logを利用して確認します。


export const Danger: Story = {
  play: async ({ canvasElement }) => {
    console.log('canvasElement', canvasElement);
    const canvas = within(canvasElement);
    console.log('canvas', canvas);
    const button = await canvas.getByRole('button');
    console.log('button', button);
    await userEvent.click(button);
  },
  args: {
    children: 'Danger',
    color: 'danger',
    handleClick: () => alert('click'),
  },
};

console.logで設定した値はブラウザのデベロッパーツールのコンソールから確認します。

canvasElementはButtonコンポーネントにdiv要素がラップされている要素です。


<div id="storybook-root">
  <div id="storybook-root"><button class="danger base">Danger</button></div>
</div>

canvasElementをwithin関数でラップすることでさまざまな関数を持つオブジェクトであることがわかります。それらの関数はコンポーネント上の要素を見つけるためのQuery関数として利用します。

canvasの中身
canvasの中身

canvasオブジェクトの中の関数getByRoleを利用して取得したbuttonはbutton要素であることがわかります。


<button class="danger base">Danger</button>

useEventのclickメソッドの引数にbutton要素を設定することでクリックが行われています。

テスト

Test runner

Play Functionを追加した時にInteractionには動作確認の成功を表す”PASS”が表示されていました。複数のストーリーにPlay Functionを設定した場合に一つ一つブラウザ上で動作確認テストを行うのは効率的ではありません。Test runnerを利用することでテストを一括で行うことができます。

Test runnerではJest, PlayWrightを利用して行っています。テストはPlay Functionが記述されたstoriesだけを実行するのではなくすべてのstoriesファイルで行われます。Play Functionがない場合は各ストーリーがエラーなしで描写されるのか確認を行います。

@test-runnerのインストールを行います。


% npm install @storybook/test-runner --save-dev

インストールが完了したらpackage.jsonファイルのscriptに実行するためにコマンドを追加します。


{
  "name": "react-storybook7",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview",
    "storybook": "storybook dev -p 6006",
    "build-storybook": "storybook build",
    "test-storybook": "test-storybook"
  },
//略

test-runnerを実行するための設定は完了です。package.jsonファイルに追加した後はnpm run test-storybookコマンドでTest runnerを実行することができます。

test-runnerを実行するためにはStorybookを起動しておかなければなりません。起動していない場合は下記のエラーメッセージが表示されます。


 % npm run test-storybook

> react-storybook7@0.0.0 test-storybook
> test-storybook

[test-storybook] It seems that your Storybook instance is not running at: http://127.0.0.1:6006. Are you sure it's running?

実際に実行されると以下のように表示されます。すべてのテストにPASSしたことがわかります。


% npm run test-storybook

> react-storybook7@0.0.0 test-storybook
> test-storybook

 PASS   browser: chromium  src/components/UserItem.stories.tsx (5.745 s)
 PASS   browser: chromium  src/components/UserList.stories.tsx (5.78 s)
 PASS   browser: chromium  src/components/Button.stories.tsx (5.789 s)

Test Suites: 3 passed, 3 total
Tests:       8 passed, 8 total
Snapshots:   0 total
Time:        9.923 s
Ran all test suites.

verboseモード

もう少し行われたテストの内容を確認したい場合にはオプションの–verboseをpackage.jsonで追加します。


"test-storybook": "test-storybook --verbose"

設定後再度”npm run test-storybook”を実行します。


 % npm run test-storybook

> react-storybook7@0.0.0 test-storybook
> test-storybook --verbose

 PASS   browser: chromium  src/components/UserList.stories.tsx
  UserList
    FetchData
      ✓ smoke-test (285 ms)

 PASS   browser: chromium  src/components/Button.stories.tsx
  Button
    Default
      ✓ smoke-test (198 ms)
    Primary
      ✓ smoke-test (19 ms)
    Danger
      ✓ play-test (66 ms)
    PrimarySmall
      ✓ smoke-test (35 ms)
    PrimaryLarge
      ✓ smoke-test (17 ms)

 PASS   browser: chromium  src/components/UserItem.stories.tsx
  UserItem
    FetchData
      ✓ smoke-test (625 ms)
    Default
      ✓ smoke-test (31 ms)

Test Suites: 3 passed, 3 total
Tests:       8 passed, 8 total
Snapshots:   0 total
Time:        7.871 s

Play Functionが含まれるButtonのDangerには”play test”と表示され残りのストーリーには”smoke-test”と表示されています。Interationsのテストが実行されたのか識別することができます。

watchAllモード

–watchAllオプションをつけることでファイルの更新を検知して自動でテストを再実行してくれます。

–watchオプションもありますが–watchを利用の場合には”–watch is not supported without git/hg, please use –watchAll”のメッセージが表示されるのでwatchAllを利用しています。

Canvas上の表示設定

Canvas上の表示設定はpreview.tsを利用したグローバルレベル、metaで設定を行うコンポーネントレベル、Story担任で設定するストーリーレベルで行うことができます。

Parametersによる背景の設定

ToolbarのアイコンをクリックすることでCanvasの背景色を変更することができます。デフォルトでは”light”と”dark”を切り替えることができます。

背景色の設定
背景色の設定

darkを選択すると下記のようにcanvasの背景が変わります。

ダークモード
ダークモード

その他の背景色に設定したい場合にはmetaにparametersを追加した行うことができます。


const meta = {
  title: 'Button',
  component: Button,
  tags: ['autodocs'],
  parameters: {
    backgrounds: {
      values: [
        { name: 'red', value: '#f00' },
        { name: 'green', value: '#0f0' },
        { name: 'blue', value: '#00f' },
      ],
    },
  },
} satisfies Meta<typeof Button>;

light, darkから設定したred, green, blueの選択項目が変わります。

背景色の選択
背景色の選択

背景色の設定はストーリー単位でも行うことができます。


export const Primary: Story = {
  args: {
    children: 'Primary',
    color: 'primary',
  },
  parameters: {
    backgrounds: {
      values: [
        { name: 'red', value: '#f00' },
        { name: 'green', value: '#0f0' },
        { name: 'blue', value: '#00f' },
      ],
    },
  },
};

Decoratorsの設定

Decoratorsを利用することでCanvasに表示されているButtonコンポーネントの外側にスタイルを設定することができます。

現在左上に表示されているButtonコンポーネントにmarginを設定したい場合には以下のようにDecoratorsを利用することで実現することができます。


const meta = {
  title: 'Button',
  component: Button,
  tags: ['autodocs'],
  decorators: [
    (Story) => (
      <div style={{ margin: '3em' }}>
        <Story />
      </div>
    ),
  ],
} satisfies Meta<typeof Button>;

Decoratorsの設定でボタンの周りにmarginが設定されています。

Decoratorsを利用してmarginを追加
Decoratorsを利用してmarginを追加

layoutの設定

Decoratorsを利用してスタイルを設定することで真ん中に表示させることもできますがlayoutを利用すると簡単に中央表示にすることができます。


const meta = {
  title: 'Button',
  component: Button,
  tags: ['autodocs'],
  parameters: {
    layout: 'centered',
  },
} satisfies Meta<typeof Button>;

canvasの中央に表示されます。

中央表示
中央表示

layoutに値にはcenteredの他にdefaultのpaddedとfullscreenがありますがpaddedとfullscreenの違いがpaddingがあるかどうかの違いです。fullscreenで画面一杯にコンポーネントが広がるわけではありません。

Viewportの設定

toolbarにあるアイコンをクリックすることでデフォルトでは”Small mobile”, ”Large mobile”, ”Table”とデフォルトの4つのviewportのサイズでコンポーネントのデザインを確認することができます。

toolbarのアイコンをクリック
toolbarのアイコンをクリック

“Small mobile”に変更するとモバイルのサイズに合わせた幅で表示されます。viewportによりレスポンシブデザインの表示確認を行うことができます。

Small mobileサイズ
Small mobileサイズ