StorybookはアプリケーションからUIコンポーネントを切り離し、独立した状態でコンポーネントの開発、動作確認やテストを行うために利用することができるUIツールです。コンポーネントが持つpropsを変更することでブラウザ上でどのような表示になるかプレビューを行うことで動作確認することができます。この説明が最初は”?”の人でも本文書では公式のドキュメントよりもさらにシンプルなコードを利用して動作確認を行なっているのでStorybookの基礎的な利用方法や機能を短時間で理解することができます。

Storybookが利用できるJavaScriptのフレームワーク/ライブラリにはReact以外にもVue.js, React Native, Svelte, Amber, Angularなどがありますが本文書ではReactを利用しています。本文書で動作確認を行なっているStorybookのバージョンは6です。

Reactのインストール

create-react-appを利用してreact-storybookという名前のReactプロジェクトを作成します。プロジェクト名は任意の名前をつけてください。


 % npx create-react-app react-storybook

プロジェクトの作成完了後、react-storybookフォルダに移動します。


 % cd react-storybook

StoryBookのインストール

react-storybookフォルダでnpx storybook@latest initコマンドを実行してstorybookのインストールを行います。


 % npx storybook@latestyn init
Need to install the following packages:
  storybook
Ok to proceed? (y) 
//略
✅ migration check successfully ran


To run your Storybook, type:

   yarn storybook 

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

インストールが完了したらインストールメッセージに表示されている通りyarn strorybookを実行します。起動メッセージにバージョン(6.5.5)が表示されます。本文書ではStorybookのバージョン6.5.5を利用して動作確認を行います。Storybookの最新バージョンは7です。


 % yarn strorybook
//略
99% done plugins webpack-hot-middlewarewebpack built preview b835dc28a07a2114afea in 15221ms
╭────────────────────────────────────────────────────╮
│                                                    │
│   Storybook 6.5.5 for React started                │
│   14 s for manager and 17 s for preview            │
│                                                    │
│    Local:            http://localhost:6006/        │
│    On your network:  http://192.168.2.110:6006/    │
│                                                    │
╰────────────────────────────────────────────────────╯
<i> [webpack-dev-middleware] wait until bundle finished: /
99% done plugins webpack-hot-middlewarewebpack built preview 27f8a5b932c644b98baa in 1200ms
npm run storybookでも起動します。

インストールが完了するとブラウザが自動で起動しWelcomeページが表示されます。

Welcomeページの表示
Welcomeページの表示

左のメニューにあるButtonを展開するとLarge, Primary, Secondary, Smallが表示されLargeをクリックすると大きなボタン、Primaryだとブルーカラーのボタン、Smallだと小さなボタンというように同じボタンでも異なるデザインのボタンが表示されます。左のメニューに表示されているButton, Header, Pageなどはコンポーネントに対応する名前で、Buttonの下の階層にあるLarge, Primary,…などをStorybookではストーリーと呼びます。1つのコンポーネントに対して複数のストーリーを設定すると各ストーリーで設定した値を元にサイドメニューの右側のCanvasタブにコンポーネントが描写されます。

Buttonを確認
Buttonを確認

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

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

storiesフォルダの中身を確認
storiesフォルダの中身を確認

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

初めてのStoryBook設定

Buttonコンポーネントの設定

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

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

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


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

export default Button;

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

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


import Button from './Button';

export default {
  title: 'Button',
  component: Button,
};

export defaultでmetadataを設定後その下にストーリーを記述します。ストーリーとしてHelloButtonを定義しています。HelloButton関数の中ではButtonタグの間に文字列を挿入し、挿入した文字列はprops.childrenでButtonコンポーネントに渡しています。


import Button from './Button';

export default {
  title: 'Button',
  component: Button,
};

export const HelloButton = () => <Button>Hello World!</Button>;

これでButtonコンポーネントに対するストーリーの設定は完了です。ブラウザの左側のメニューに追加したButtonの情報が表示されます。

追加したButtonの表示
追加したButtonの表示

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

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

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


module.exports = {
  // "stories": [
  //   "../src/**/*.stories.mdx",
  //   "../src/**/*.stories.@(js|jsx|ts|tsx)"
  // ],
  stories: ['../src/components/**/*.stories.jsx'], //追加
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
    '@storybook/preset-create-react-app',
  ],
  framework: '@storybook/react',
  core: {
    builder: '@storybook/builder-webpack5',
  },
};

設定ファイルのmain.jsを更新した場合はStorybookを再起動する必要があるのでyarn storybookを再実行してください。

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

ブラウザからの確認
ブラウザからの確認

ボタンに表示される文字を変えたい場合には同じファイル(button.stories.jsx)の中に新たにストーリーを追加することでテキストの変更によってどのような違いができるのかを確認することができます。


import Button from '../Button/Button';

export default {
  title: 'Button',
  component: Button,
};

export const HelloButton = () => <Button>Hello World!</Button>;
export const ClickButton = () => <Button>Click!</Button>;

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

ClickButtonの確認
ClickButtonの確認

titleの変更

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


export default {
  title: 'Common/Button',
  component: Button,
};

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

titleによる階層化
titleによる階層化

titleを設定しない場合はStrobookが自動で設定を行なってくれます。

.storybookフォルダの確認

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


module.exports = {
  // "stories": [
  //   "../src/**/*.stories.mdx",
  //   "../src/**/*.stories.@(js|jsx|ts|tsx)"
  // ],
  stories: ['../src/components/**/*.stories.jsx'],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
    '@storybook/preset-create-react-app',
  ],
  framework: '@storybook/react',
  core: {
    builder: '@storybook/builder-webpack5',
  },
};

addonsの確認

addonsによるStorybookの機能の拡張といってもどのようなことが可能なのかわかりにくいかと思います。addonsの影響によってどのような変化があるのか確認しておきましょう。

デフォルトの状態のブラウザ上に表示されるStorybookの画面を見ると上部はさまざまなアイコン、下部にはControlsやActoinsなどのタブを確認することができます。

デフォルトの状態の画面
デフォルトの状態の画面

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


module.exports = {
  // "stories": [
  //   "../src/**/*.stories.mdx",
  //   "../src/**/*.stories.@(js|jsx|ts|tsx)"
  // ],
  stories: ['../src/components/**/*.stories.jsx'],
  addons: [
    '@storybook/addon-links',
    // '@storybook/addon-essentials',
    '@storybook/addon-interactions',
    '@storybook/preset-create-react-app',
  ],
  framework: '@storybook/react',
  core: {
    builder: '@storybook/builder-webpack5',
  },
};

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

ブラウザを確認するとCanvasの横のDocsが消え、上部に複数表示されていたアイコンの数が5つ消え下部のタグもInteractionsのみの表示になっています。

essentialのaddonsを利用していない場合
essentialのaddonsを利用していない場合

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

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

ストーリーの追加

CSSの適用

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


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

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


import './button.css';
function Button({ children }) {
  return <button>{children}</button>;
}

export default Button;

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

CSSの適用
CSSの適用

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


import './button.css';
function Button({ children, color = 'default' }) {
  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のcolorを設定
propsのcolorを設定

propsのcolorの値によってボタンのデザインが変わるようにbutton.stories.jsxで設定したストーリーを変更します。


import Button from '../Button/Button';

export default {
  title: 'Common/Button',
  component: Button,
};

export const Default = () => <Button>Default</Button>;
export const Primary = () => <Button color="primary">Primary</Button>;

Primary関数の場合にはButtonタグのcolor propsにprimaryを設定していますがprimaryはbutton.cssに存在しないのでbutton.cssに追加します。


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

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

Primaryを選択した場合
Primaryを選択した場合

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


export const Default = () => <Button>Default</Button>;
export const Primary = () => <Button color="primary">Primary</Button>;
export const Danger = () => <Button color="danger">Danger</Button>;

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


//略
.danger {
  background-color: #dc3545;
}
ストーリーのDangaerを追加
ストーリーのDangaerを追加

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

サイズの変更

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


import './button.css';
function Button({ children, color = 'default', size = 'base' }) {
  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.jsxにカラーとサイズを指定したストーリー(PrimarySmall, PrimaryLarge)を追加します。


import Button from '../Button/Button';

export default {
  title: 'Common/Button',
  component: Button,
};

export const Default = () => <Button>Default</Button>;
export const Danger = () => <Button color="danger">Danger</Button>;
export const Primary = () => <Button color="primary">Primary</Button>;
export const PrimarySmall = () => (
  <Button size="sm" color="primary">
    Small
  </Button>
);
export const PrimaryLarge = () => (
  <Button size="lg" color="primary">
    Large
  </Button>
);

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

Primary Largeを選択
Primary Largeを選択

Argsの利用

これまでの設定でもストーリーを設定することができましたがより柔軟にストーリーを記述するためにArgsを利用した記述方法を確認します。Argsはargumentsの略です。

Argsを利用する場合はまず最初にTemplate関数を設定します。argsはコンポーネントに渡すpropsです。


const Template = (args) => <Button {...args} />;

ストーリーのDefault関数をTemplate関数でbindすることで新規の関数として作成します。作成後はargsを利用してpropsの値を設定します。


const Template = (args) => <Button {...args} />
export const Default = Template.bind({});
Default.args = {
  children: 'Default',
};

同様の方法でほかのストーリーも変更を行います。


const Template = (args) => <Button {...args} />;
export const Default = Template.bind({});
Default.args = {
  children: 'Default',
};

export const Danger = Template.bind({});
Danger.args = {
  children: 'Danger',
  color: 'danger',
};

export const Primary = Template.bind({});
Primary.args = {
  children: 'Primary',
  color: 'primary',
};

export const PrimarySmall = Template.bind({});
PrimarySmall.args = {
  children: 'Small',
  color: 'primary',
  size: 'sm',
};

export const PrimaryLarge = Template.bind({});
PrimaryLarge.args = {
  children: 'Large',
  color: 'primary',
  size: 'lg',
};

ブラウザ上に表示されるボタンはArgsに変更する前と何も変わりません。

Argsの値は他のストーリーで利用した値を利用することができます。例えばPrimarySmallとPrimaryLargeはchildren, sizeが異なりcolorは同じ値を設定しています。PrimaryLarge.argsの設定でSpread Operatorを利用してPrimarySmall.argsの値を利用することができます。propsが多い場合には便利です。


export const PrimaryLarge = Template.bind({});
PrimaryLarge.args = {
  ...PrimarySmall.args,
  children: 'Large',
  size: 'lg',
};

// export const PrimaryLarge = Template.bind({});
// PrimaryLarge.args = {
//   children: 'Large',
//   color: 'primary',
//   size: 'lg',
// };

Storybookが持つ機能の確認

Controlsの利用

Argsの設定を行うと下部に表示されていたControlsが利用できるようになります。

Controlsを利用することで現在ブラウザ上に表示されているボタンを変更することができます。下記の画面ではPrimaryを選択しているのでストーリーを選択した直後では背景色はブルーでボタンの文字列はPrimaryですがControlsを通して設定を変更することで背景色が赤でボタンの文字列をPrimaryDangerに変更することができます。

Controlsを通してボタンを変更
Controlsを通してボタンを変更

DefaultのストーリーをArgsを利用しない場合のDefault関数に戻すとControlsは下記のように表示され変更することはできません。


export const Default = () => <Button>Default</Button>;
Argsを設定していない場合
Argsを設定していない場合

PropTypesによる設定

Argsによってボタンに表示されるテキストやcolorをブラウザ上で変更できるようになりました。しかしcolor, sizeは手動で入力する必要があります。

React/JavaScriptの場合PropTypesを設定することでpropsで渡される型の設定を行うことができます。

実際にButton.jsxファイルの中でPropTypesを設定することでcolorやsizeで選択できる値に制限をかけることができます。下記ではsizeはsm, base, lg, colorはprimary, default, dangerのみ選択できるように設定しています。


import './button.css';
import PropTypes from 'prop-types';

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

export default Button;

Button.propTypes = {
  color: PropTypes.oneOf(['primary', 'default', 'danger']),
  size: PropTypes.oneOf(['sm', 'base', 'lg']),
};

ブラウザで確認すると先ほどまでとは異なり、ラジオボタンでcolor, sizeを選択できるようになりました。ラジオボタンで別のcolorを選択すると即座にボタンの色が変わります。

PropTypes設定により選択の制限
PropTypes設定により選択の制限

ラジオボタンの変更すると変更した値はCanvasのボタンの描写に反映されます。

argTypesによる設定

PropTypesではなくButton.stories.jsxファイルでargTypesを利用することでcolor, sizeをControlsで変更できるようにすることもできます。


import Button from './Button';

export default {
  title: 'Common/Test',
  component: Button,
  argTypes: {
    color: {
      options: ['primary', 'default', 'danger'],
      control: { type: 'radio' },
    },
    size: {
      options: ['sm', 'base', 'lg'],
      control: { type: 'radio' },
    },
  },
};

//略

Button.jsxファイルで追加したButton.propTypesを削除しても下記のように表示されます。

PropTypes設定により選択の制限
PropTypes設定により選択の制限

typeをradioからselectに変更するとラジオボタンではなくセレクトボックスに変更されます。


import Button from './Button';

export default {
  title: 'Common/Test',
  component: Button,
  argTypes: {
    color: {
      options: ['primary', 'default', 'danger'],
      control: { type: 'select' },
    },
    size: {
      options: ['sm', 'base', 'lg'],
      control: { type: 'select' },
    },
  },
};

//略

下記ではcolorを選択しています。

typeをradioからselectへ
typeをradioからselectへ

typeをradioに設定するとラジオボタン、selectに設定するとセレクトボックスで値を設定することができました。他にもtypeにはcolorを設定することができます。

Buttonコンポーネントのpropsに新たにボタンの背景を設定できるbackgroundColorを追加します。backgroundColorの値が渡された場合のみ設定が行われます。


import './button.css';
function Button({
  children,
  color = 'default',
  size = 'base',
  backgroundColor,
}) {
  return (
    <button
      className={`${color} ${size}`}
      style={backgroundColor && { backgroundColor }}
    >
      {children}
    </button>
  );
}

export default Button;

argTypesでbackgroundColorのtypeに”color”を選択します。


export default {
  title: 'Common/Button',
  component: Button,
  argTypes: {
    color: {
      options: ['primary', 'default', 'danger'],
      control: { type: 'radio' },
    },
    size: {
      options: ['sm', 'base', 'lg'],
      control: { type: 'select' },
    },
    backgroundColor: {
      control: { type: 'color' },
    },
  },
};

color pickerが表示されカラーを選択することができます。

typeにcolorを設定
typeにcolorを設定

typeにはこの他にはtext, date, boolean, numberなどあるのでドキュメントのControlsのAnnotationを確認してください。

Actionsの確認(PropTypes)

ButtonコンポーネントにonClickイベントを設定しpropsから渡された関数を設定した場合にActionsによりイベントを検知することでボタンのonClickイベントが正常に動作するのかどうか確認することができます。

propsで渡されるonClick関数をButtonコンポーネントのbutton要素のonClickイベントに設定します。


import './button.css';
import PropTypes from 'prop-types';

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

export default Button;

Button.propTypes = {
  color: PropTypes.oneOf(['primary', 'default', 'danger']),
  size: PropTypes.oneOf(['sm', 'base', 'lg']),
};

Actionsが正常に動作している場合はボタンをクリックすると下部のActionsタブにメッセージが表示されますが現在の設定では何も反応がありません。

Clickに対する反応なし
Clickに対する反応なし

PropTypesの設定にonClickを追加します。


Button.propTypes = {
  color: PropTypes.oneOf(['primary', 'default', 'danger']),
  size: PropTypes.oneOf(['sm', 'base', 'lg']),
  onClick: PropTypes.func.isRequired,
};

再度ボタンをクリックするとクリックに発火するイベントを検知してメッセージが表示されます。下記では3回ボタンをクリックしています。

Clickイベントの検知
Clickイベントの検知

関数の名前をonClickとしていましたが名前は必ずしもonClickではないのでpropsで渡す関数の名前をonClickからhandleClickに変更します。


//略
function Button({ children, color = 'default', size = 'base', handleClick }) {
  return (
    <button className={`${color} ${size}`} onClick={handleClick}>
//略

変更するとまたonClickイベントが検知できなくなります。その理由は.storybookフォルダのStorybookの設定ファイルの一つであるpreview.jsファイルにactionsに関する設定が行われておりonから開始する関数名のみ検知することができるためです。


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

handleClick関数の名前を変えず、preview.jsファイルの設定を変えずにclickイベントを検知できるように設定を行います。ButtonコンポーネントのhandleClick関数だけ検知できようにするためには以下のようにmetadataの中にargTypesの設定を行うことで実現できます。


export default {
  title: 'Common/Button',
  component: Button,
  argTypes: { handleClick: { action: 'clicked' } },
};

argTypesの中のactionで指定した’clicked’はActionsタブのメッセージの先頭に表示されます。

argTypes設定後の動作確認
argTypes設定後の動作確認

actionの値をtrueに変更すると関数名が表示されます。


export default {
  title: 'Common/Button',
  component: Button,
  argTypes: { handleClick: { action: true } },
};

1回目は先ほどの”clicked”設定時のメッセージで2回目がactionの値をtrueにした後のメッセージです。上のメッセージの方が新しいメッセージです。

actionの値の変更によるメッセージへの影響
actionの値の変更によるメッセージへの影響

Actionsの確認(argTypes)

先ほどはpropTypesにonClickを追加しましたがActionsの設定はButton.stories.jsxのみで設定を行うこともできます。ButtonコンポーネントにonClickイベントを設定しpropsから渡された関数を設定した場合にActionsによりボタンのクリックを検知することができます。


import './button.css';

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

export default Button;

argTypesにonClickを設定します。


export default {
  title: 'Common/Button',
  component: Button,
  argTypes: {
    color: {
      options: ['primary', 'default', 'danger'],
      control: { type: 'radio' },
    },
    size: {
      options: ['sm', 'base', 'lg'],
      control: { type: 'radio' },
    },
    onClick: {
      action: 'clicked',
    },
  },
};

設定後ボタンをクリックするとクリックを検知してメッセージが表示されます。下記では3回ボタンをクリックしています。

Clickイベントの検知
Clickイベントの検知

propsで渡す名前をonClickからhandleClickに変更します。


import './button.css';
function Button({ children, color = 'default', size = 'base', handleClick }) {
  return (
    <button className={`${color} ${size}`} onClick={handleClick}>
      {children}
    </button>
  );
}

export default Button;

変更した場合はargTypesでもonClickからhandleClickに変更する必要があります。


export default {
  title: 'Common/Button',
  component: Button,
  argTypes: {
//略
    handleClick: {
      action: 'clicked',
    },
  },
};

actionの値をtrueに変更すると関数名が表示されます。


export default {
  title: 'Common/Button',
  component: Button,
  argTypes: {
//略
    handleClick: {
      action: true,
    },
  },
};

1回目は先ほどの”clicked”設定時のメッセージで2回目がactionの値をtrueにした後のメッセージです。上のメッセージの方が新しいメッセージです。

actionの値の変更によるメッセージへの影響
actionの値の変更によるメッセージへの影響

argTypesを利用しないでActions

metadataのargTypesを利用せず@storybook/addon-actions’からimportしたactionを利用してActionsタグにメッセージを表示させることができます。


import { action } from '@storybook/addon-actions';
import Button from './Button';

export default {
  title: 'Common/Test',
  component: Button,
  argTypes: {
    color: {
      options: ['primary', 'default', 'danger'],
      control: { type: 'radio' },
    },
    size: {
      options: ['sm', 'base', 'lg'],
      control: { type: 'select' },
    },
  },
};

const something = action('something');

const Template = (args) => {
  const handleClick = () => {
    something();
  };
  return <Button {...args} handleClick={handleClick} />;
};
//略

利用しているBottonコンポーネントの設定は下記の通りです。


import './button.css';
function Button({ children, color = 'default', size = 'base', handleClick }) {
  return (
    <button className={`${color} ${size}`} onClick={handleClick}>
      {children}
    </button>
  );
}

export default Button;

設定後ブラウザ上のボタンをクリックするとactionの引数に設定した文字列が表示されます。

Actionsタグのメッセージの確認
Actionsタグのメッセージの確認

something関数の引数にevent(e)を設定するとActionsのメッセージにイベントの情報も表示されます。


import { action } from '@storybook/addon-actions';
//略
const something = action('something');

const Template = (args) => {
  const handleClick = (e) => {
    something(e);
  };
  return <Button {...args} handleClick={handleClick} />;
};
引数の設定
引数の設定

引数は複数設定することができます。


const handleClick = (e) => {
  something(e, 'test');
};

ボタンをクリック後にActionsのタブを確認すると以下のメッセージが表示されます。eventと文字列’test’の情報が含まれています。

関数の引数を複数設定した場合のメッセージ
関数の引数を複数設定した場合のメッセージ

LinkToの設定

ブラウザ上のあるストーリーから別のストーリーに移動する際にLinkToを利用することができます。

ボタンをクリックすると移動できるように設定を行っていきます。利用するボタンコンポーネントは下記の通りです。


import './button.css';
function Button({ children, color = 'default', size = 'base', handleClick }) {
  return (
    <button className={`${color} ${size}`} onClick={handleClick}>
      {children}
    </button>
  );
}

export default Button;

handleClickを利用します。linkToの第一引数には移動先のコンポーネントのtitleを設定、第2引数には移動先のストーリーの名前を設定しています。


import { linkTo } from '@storybook/addon-links';
//略
const Template = (args) => {
  return <Button {...args} handleClick={linkTo('Common/Button', 'Danger')} />;
};
export const Default = Template.bind({});
Default.args = {
  children: 'Default',
};

以上でLinkToの設定は完了です。

Defaultのストーリーに表示されているボタンをクリックすつとDangerのストーリーに移動します。Templateに設定を行っているためDefault以外のストーリーでもボタンをクリックするとすべて Dangerのストーリーに移動します。

Interactionsの設定

Interactionsはストーリーでコンポーネントが描写された後にユーザによるインタラクションをシュミレートしたい時に利用することができるaddonです。ボタンコンポーネントで利用した場合はストーリーの中でボタンをクリックすることができます。

Interactionsではtesting-library、jestの機能を利用します。そのほかにaddonsの@storybook/addon-interactionsが必要になります。デフォルトで@storybook/addon-interactionsとstorybook/testing-libraryのインストールは行われているのでstorybook/jestのみ追加でインストールします。jestの機能を利用しない場合は必要ありません。ここではjestのexpect関数を利用するのでインストールしています。


 % npm install @storybook/jest

InterationsはPrimaryLargeストーリーを利用して行います。Interactionsの設定は下記のようにplay関数の中に記述します。引数でargsとcanvasElementを受け取ることができます。


export default {
  title: 'Common/Button',
  component: Button,
  argTypes: {
//略
    handleClick: {
      action: true,
    },
  },
};
const Template = (args) => {
  return <Button {...args} />;
};
//略
PrimaryLarge.play = async ({ args, canvasElement }) => {
  //処理
};

PrimaryLarge.playの関数の中でconsole.logを利用してargsとcanvasElementの中身を確認しておきます。今回のButtonコンポーネントであればargsは以下のオブジェクトです。


{
  children: "Large"
  color: "primary"
  handleClick: ƒ ()
  size: "lg"
}

canvasElementはidにrootの値を持つdiv要素で囲まれたButtonコンポーネントのDOMです。


<div id="root">
  <button class="primary lg">Large</button>
</div>

button要素に取得する際に利用するgetByRoleメソッドを利用するためにwithin関数の引数にcanvasElementを指定します。取得したボタンのクリックイベントを実行するためにuserEventのclickメソッドを実行します。実行後にhandleClick関数が実行されたかMatcher関数のtoHaveBeenCalled関数でチェックを行なっています。


import Button from './Button';
import { userEvent, within } from "@storybook/testing-library";
import { expect } from '@storybook/jest';
//略
PrimaryLarge.play = async ({ args, canvasElement }) => {
  const canvas = within(canvasElement);
  await userEvent.click(canvas.getByRole('button'));
  await expect(args.handleClick).toHaveBeenCalled();
};

ブラウザ上でPrimaryLargeストーリーを選択した後にplay関数が実行され下部のInterationsのタブに2件のメッセージが表示されます。

Interationsタグにメッセージが表示
Interationsタグにメッセージが表示

Actionsのタグを見るとクリックが行われているので1件メッセージが表示されます。

Actionsタブの確認
Actionsタブの確認

ここでのInteractionsはクリックを実行し、クリックが実行されたかどうかのチェックを行っているだけですがInteractionsによってボタンの見た目が変わるようにButton.jsxファイルの設定を変更します。

useState Hookでmessageを定義します。handleClick関数はpropsで渡されるのではなくButtonコンポーネントの中で定義し、実行するとclickedがmessageに設定されるようにしています。


import './button.css';
import PropTypes from 'prop-types';
import { useState } from 'react';

function Button({ children, color = 'default', size = 'base' }) {
  const [message, setmessage] = useState('');

  const handleClick = () => {
    setmessage('clicked');
  };
  return (
    <button className={`${color} ${size}`} onClick={handleClick}>
      {children} {message}
    </button>
  );
}

export default Button;

Button.propTypes = {
  color: PropTypes.oneOf(['primary', 'default', 'danger']),
  size: PropTypes.oneOf(['sm', 'base', 'lg']),
};

PrimaryLargeストーリーのplay関数の中ではbuttonをクリックする設定のみ残してexpect関数によるチェックは削除しています。


PrimaryLarge.play = async ({ canvasElement }) => {
  const canvas = within(canvasElement);
  await userEvent.click(canvas.getByRole('button'));
};

ブラウザでPrimaryLargeストーリーを選択するとボタンはLarge clickedと表示されます。先程よりも描写されているボタンに変化があるのでInterationsを利用することでユーザとのインタラクションを行えるという説明も理解できるかと思います。

Interactionsによるボタンの変化
Interactionsによるボタンの変化

Canvasの表示設定

Parametersの設定

デフォルトの状態ではボタンが表示されているCanvasの背景色はlightかdarkのみ選択することができます。

backroundの変更はデフォルトで利用できる@storybook/addon-essentialsのaddonsに含まれています。
背景色の選択
背景色の選択

lightに設定されているのでdarkに変更すると背景色が暗くなります。

背景色をdarkに
背景色をdarkに

light, dark以外の背景色を設定したい場合にParametersを以下のように設定することができます。Primaryのストーリーのみで選択できるように設定を行っており、選択できる背景色はred, green, blueです。


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

設定後light, darkではなくred, green, blueが選択できるようになります。

Parametersで設定したカラーの選択
Parametersで設定したカラーの選択

実際にgreenを選択します。背景色がgreenになることが確認できます。

背景色をグリーンに
背景色をグリーンに

個別のストーリーではなくButton内のすべてのストーリーでParametersの選択を行いたい場合はexport defaultのmetadataの中で設定を行うことができます。


export default {
  title: 'Common/Button',
  component: Button,
  parameters: {
    backgrounds: {
      values: [
        { name: 'red', value: '#f00' },
        { name: 'green', value: '#0f0' },
        { name: 'blue', value: '#00f' },
      ],
    },
  },
};

Buttonコンポーネントに含まれるストーリーで背景色の設定が可能になります。

Decoratorsの設定

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

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


export default {
  title: 'Common/Button',
  component: Button,
  decorators: [
    (Story) => (
      <div
        style={{
          display: 'flex',
          justifyContent: 'center',
          alignItems: 'center',
          height: '100vh',
        }}
      >
        <Story />
      </div>
    ),
  ],
};

ブラウザ上ではボタンが中央に表示されることが確認できます。

Decoratorsでボタンを中央に表示
Decoratorsでボタンを中央に表示

Layoutの設定

Decoratorsを利用することでButtonコンポーネントの外側にスタイルを設定することができました。style属性で設定を行いましたが中央に表示するのであればparametersのlayoutを利用することができます。


export default {
  title: 'Common/Button',
  component: Button,
  parameters: {
    layout: 'centered',
  },
};

centeredを設定することで下記のように表示されます。

layoutにより中央表示
layoutにより中央表示

設定値には”centered”以外にデフォルトの”padded”と”fullscreen”があります。paddedではpaddingが設定されていますがfullscreenにするとpaddingがなくなります。


export default {
  title: 'Common/Button',
  component: Button,
  parameters: {
    layout: 'fullscreen',
  },
};

fullscreenでは下記のように表示されます。

layoutでfullscreenを設定
layoutでfullscreenを設定

Docsの設定

これまでCanvasのみ表示していましたがCanvasの右側にDocsがありこの機能もaddonsにより実現されています。addonsの@storybook/addon-essentialsに含まれています。

Docsではコンポーネントに関する情報を確認することができます。PropsTypeで設定したデフォルト値やcolor, sizeの選択項目も確認することができます。

Docsの確認

Button.stories.jsxファイルのexport defaultに設定したcomponentをコメントすると表示されなくなります。


export default {
  title: 'Common/Button',
  // component: Button,
};

DescriptionやDefault値が表示されていません。

Docsの情報が表示されていない画面
Docsの情報が表示されていない画面

表示される情報を増やしたい場合はButton.jsxにコメントを追加することでDocsに表示されます。


import './button.css';
import PropTypes from 'prop-types';

/**
 * Button Component for explanation of StoryBook
 */

function Button({ children, color = 'default', size = 'base', handleClick }) {
//略

記述した”Button Component for explanation of StoryBook”が表示されます。

Button.jsxに記述したコメントが表示
Button.jsxに記述したコメントが表示

propTyesの上にコメントを追加すると追加したコメントがDescriptionの中に表示されます。


Button.propTypes = {
  /**
   * What background color to use
   */
  color: PropTypes.oneOf(['primary', 'default', 'danger']),
  /**
   * How large should the button be?
   */
  size: PropTypes.oneOf(['sm', 'base', 'lg']),
};

colorのDescriptionの中に”What background color to use”、sizeのDescriptionの中に”How large should the button be?”が表示されます。

PropTypesのコメント表示
PropTypesのコメント表示

コメントについてはButton.stories.jsxファイルでも設定することができます。


export default {
  title: 'Common/Button',
  component: Button,
  parameters: {
    docs: {
      description: {
        component: '説明用のボタンコンポーネント',
      },
    },
  },
};

記述した内容が表示されます。

Button.stories.jsxファイルで記述した内容が表示
Button.stories.jsxファイルで記述した内容が表示

Exampleの確認

main.jsファイルのstoriesの値を最初の値に戻します。


module.exports = {
  stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
  // stories: ['../src/components/**/*.stories.jsx'],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
    '@storybook/preset-create-react-app',
  ],
  framework: '@storybook/react',
  core: {
    builder: '@storybook/builder-webpack5',
  },
};

これで作成したButtonコンポーネントも含めて本プロジェクトに含まれるストーリーがすべて表示されます。

デフォルトのストーリーを表示
デフォルトのストーリーを表示

本文書を読み終えた人であれば最初にstoriesフォルダのファイルに記述されていた内容がわからなかった人も理解できるようになっているはずです。ぜひ確認してもらいさらにStorybookの理解を深めてください。