🙆‍♂️

【CSF3.0対応】Next.js + CSS Modules(Sass)にStorybookを導入し、諸々のセットアップを済まそう

2022/09/18に公開

Next.jsプロジェクトの作成

今回は、TypeScriptに対応したNext.jsを基盤として利用するため、以下コマンドを実行します。

yarn create next-app --typescript

インストール完了後、pagesとstylesをsrcディレクトリに移動してください。この先の手順は、src配下にpagesやstylesディレクトリが配置されていることを前提に進めています。

Prettier + Stylelint + ESLintを含めたNext.jsの環境構築は以下を参考にしてみてください🌟
https://zenn.dev/toono_f/articles/1774dc83548079

Storybookのインストール

プロジェクトのルートディレクトリで以下コマンドを実行します。

npx sb init

途中でeslintPlugin関連の質問を聞かれるので、「y」を入力してください。

? Do you want to run the 'eslintPlugin' migration on your project? › (y/N)

インストール完了後、以下コマンドを実行し、Storybookの画面が立ち上がるか確認しましょう。

yarn storybook

http://localhost:6006/ でStorybookが問題なく起動することを確認したら、以下設定をあらかじめ追加しておきましょう。

eslintにルール設定を追加

.eslintrc.jsonに以下記述を追加します。

{
  "extends": [
    "next/core-web-vitals",
+   "plugin:storybook/recommended"
  ]
}

public配下の画像を表示させる

publicディレクトリ配下の画像をStorybook内で読み込む場合、.storybook/main.jsに以下記述を追加します。

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

Sass(SCSS)を対応させる

StoryBook側で特に何か設定していなくともCSSModulesには対応していますが、Sass(SCSS)を使いたい場合は以下コマンドを実行し、必要パッケージをインストールします。

yarn add -D css-loader sass sass-loader style-loader

上記実行後、.storybook/main.jsに以下を追加します。

.storybook/main.js
module.exports = {
  stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
  addons: [
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    "@storybook/addon-interactions",
  ],
  framework: "@storybook/react",
  core: {
    builder: "@storybook/builder-webpack5",
  },
  staticDirs: ["../public"],
+  webpackFinal: async (config) => {
+   config.module.rules.push({
+     test: /\.scss$/,
+     use: [
+       "style-loader",
+       {
+         loader: "css-loader",
+         options: {
+           modules: {
+             auto: true, // *.module.scssファイル全てを対象
+           },
+           url: false, // cssのbackgroundで設定した画像へのパスがプロジェクトルートからの絶対パスになるように設定
+         },
+       },
+       "sass-loader",
+     ],
+     include: path.resolve(__dirname, "../src/"),
+   });
+    return config;
  },
};

上記のポイントはurl: falseの設定です。本設定を行わなければ、ルート相対パスで記載されているCSSでの背景画像を取得できなくなくなるので注意してください。

global.css(scss)を適用させる

プロジェクトのglobal.cssをStorybookで読み込ませたい場合、.storybook/preview.jsに以下記述を追加します。

.storybook/preview.js
+ import "../src/styles/globals.css";

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

next/routerを対応させる

以下コマンドを実行し、必要パッケージをインストールします。

yarn add -D storybook-addon-next-router

上記実行後、.storybook/main.jsに以下を追加します。

.storybook/main.js
module.exports = {
  stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
  addons: [
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    "@storybook/addon-interactions",
+   "storybook-addon-next-router",
  ],
  framework: "@storybook/react",
  core: {
    builder: "@storybook/builder-webpack5",
  },
  staticDirs: ["../public"],
  webpackFinal: async (config) => {
    config.module.rules.push({
      test: /\.scss$/,
      use: [
        "style-loader",
        {
          loader: "css-loader",
          options: {
            modules: {
              auto: true, // *.module.scssファイル全てを対象
            },
            url: false, // cssのbackgroundで設定した画像へのパスがプロジェクトルートからの絶対パスになるように設定
          },
        },
        "sass-loader",
      ],
      include: path.resolve(__dirname, "../src/"),
    });

    config.resolve.alias["@"] = rootPath;
    return config;
  },
};

さらに、.storybook/preview.jsに以下記述を追加します。

.storybook/preview.js
+ import { RouterContext } from "next/dist/shared/lib/router-context";
import "../src/styles/globals.scss";

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

next/imageを対応させる

.storybook/preview.jsに以下記述を追加します。

.storybook/preview.js
+ import * as nextImage from "next/image";
import { RouterContext } from "next/dist/shared/lib/router-context";
import "../src/styles/globals.scss";

+ Object.defineProperty(nextImage, "default", {
+ configurable: true,
+ value: (props) => {
+   return <img {...props} />;
+ },
+ });

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

Storyファイルで絶対パスが通るようにする

StoryファイルもしくはStoryファイル内でimportしたコンポーネント内で、絶対パスで何らかのファイルをimportしている場合、以下のようなエラーが発生します(ModuleNotFoundError)

ModuleNotFoundError: Module not found: Error: Can't resolve 'src/components/atoms/Button/Button.module.scss' in '/Users/~中略~/src/components/atoms/Button'

上記エラーを回避し絶対パスを通すために.storybook/main.jsに以下記述をそれぞれ追加します。

.storybook/main.js
+ const path = require("path");
+ const rootPath = path.resolve(__dirname, "../src/");

module.exports = {
  stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
  addons: [
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    "@storybook/addon-interactions",
    "storybook-addon-next-router",
  ],
  framework: "@storybook/react",
  core: {
    builder: "@storybook/builder-webpack5",
  },
  staticDirs: ["../public"],
  webpackFinal: async (config) => {
    config.module.rules.push({
      test: /\.scss$/,
      use: [
        "style-loader",
        {
          loader: "css-loader",
          options: {
            modules: {
              auto: true, // *.module.scssファイル全てを対象
            },
            url: false, // cssのbackgroundで設定した画像へのパスがプロジェクトルートからの絶対パスになるように設定
          },
        },
        "sass-loader",
      ],
      include: path.resolve(__dirname, "../src/"),
    });
+   config.resolve.alias["@"] = rootPath; // srcを@と省略してパスを記載できるように設定
    return config;
  },
};

ウェブアクセシビリティチェックに便利なプラグインを導入する

以下コマンドを実行し、必要パッケージをインストールします。

yarn add -D @storybook/addon-a11y

上記実行後、.storybook/main.jsに以下を追加します。

.storybook/main.js
const path = require("path");
const rootPath = path.resolve(__dirname, "../src/");

module.exports = {
  stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
    addons: [
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    "@storybook/addon-interactions",
    "storybook-addon-next-router",
+   "@storybook/addon-a11y",
  ],
  framework: "@storybook/react",
  core: {
    builder: "@storybook/builder-webpack5",
  },
  staticDirs: ["../public"],
  webpackFinal: async (config) => {
    config.module.rules.push({
      test: /\.scss$/,
      use: [
        "style-loader",
        {
          loader: "css-loader",
          options: {
            modules: {
              auto: true, // *.module.scssファイル全てを対象
            },
            url: false, // cssのbackgroundで設定した画像へのパスがプロジェクトルートからの絶対パスになるように設定
          },
        },
        "sass-loader",
      ],
      include: path.resolve(__dirname, "../src/"),
    });
   config.resolve.alias["@"] = rootPath; // srcを@と省略してパスを記載できるように設定
    return config;
  },
};

https://storybook.js.org/addons/@storybook/addon-a11y

Googleフォント(CDN)を利用する場合

.storybookディレクトリ配下にpreview-head.htmlを作成し、以下のような記述を追加する。

.storybook/preview-head.html
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700;900&family=Noto+Sans+JP:wght@300;400;500;700&display=swap" />
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons+Outlined" />

実際にStorybookにコンポーネントを追加してみる

今回は例として、Buttonコンポーネントを作成し、Storyファイルで読み込ませてStorybookで表示されるように実装を進めていきます。

Buttonコンポーネントを作成

src下にcomponentsディレクトリを作成し、その配下でButtonコンポーネントを作成します。

src/components/atoms/Button/Button.tsx
import Link from "next/link";
import styles from "./Button.module.scss";

type ButtonProps = {
  href: string;
  children: React.ReactNode;
};

export const Button = ({ href, children }: ButtonProps) => {
  return (
    <Link href={href}>
      <a className={styles.button}>
        {children}
      </a>
    </Link>
  );
};

CSSModules(Sass)を作成する

上記のButtonコンポーネントを配置したディレクトリ下にButton.module.scssを作成します。今回はscss記法を実現したかったので、cssファイルではなくscssファイルをCSSModulesとして利用しています。

src/components/atoms/Button/Button.module.scss
.button {
  display: inline-block;
  padding: 16px 32px;
  font-weight: 500;
  color: #fff;
  background-color: #3ea8ff;
  border-radius: 10px;
  box-shadow: 0px 0px 15px -5px #0f83fd;
  &:hover {
    opacity: 0.7;
  }
}

Storyファイルを作成する

上記のButtonコンポーネントを配置したディレクトリ下にButton.stories.tsxを作成し、以下のように実装します。今回はCSF3.0による実装方法を採用しています。

src/components/atoms/Button/Button.stories.tsx
import { ComponentMeta, ComponentStoryObj } from "@storybook/react";
import { Button } from "./Button";

export default { component: Button } as ComponentMeta<typeof Button>;

export const Index: ComponentStoryObj<typeof Button> = {
  args: {
    href: "/",
    children: "Button",
  },
};

Storybookを起動する

以下コマンドを実行し、Storybookを起動します。

yarn storybook

http://localhost:6006/ でStorybookが立ち上がったら、以下のような画面で作成したButtonコンポーネントが確認できるはずです。

StoryShotsを導入する

続きは以下からどうぞ。

https://zenn.dev/toono_f/articles/4d1dc926c4e041

以上です。お疲れ様でした🙏

Discussion