Storybookを使っていると「デザイナーにUIを確認してもらう場所がない」「Storybookをどこにホスティングするか」という課題に必ずぶつかります。Chromaticはこの2つをまとめて解決できるSaaSですが、料金がスナップショット数の従量制なので、設定を間違えると想定外の消費になります。

この記事では、ChromaticをGitHubのPRに組み込む方法、デザイナーがUIにコメントを入れるレビューの流れ、そしてスナップショット課金を抑えるための設定を整理します。

前提: Storybookが動いているNext.js + TypeScriptのリポジトリ(private)、CIはGitHub Actions。
以下のスクリーンショットは、検証用に立てたサンドボックスのものです。画面・社名・担当者名・金額はすべて動作確認のために作ったダミーで、実在のサービスや企業とは関係ありません。

そもそもStorybookとは

Storybookは、UIコンポーネントをアプリ本体から切り離して1つずつ表示・確認できるカタログを作るツールです。

左のサイドバーがコンポーネントの一覧です。Button の下に Default / Secondary / Outline / Ghost / Destructive / Disabled / With Icon … と並んでいるのがストーリーで、1つ1つが「このコンポーネントのこの状態」を表します。

右側には各ストーリーが実際にレンダリングされて並びます。アプリを起動して該当の画面まで操作しなくても、見たい状態をすぐ確認できるのが利点です。propsを画面上で差し替えて挙動を試すこともできます。

Chromaticは、このストーリー1つ1つをテストとレビューの単位として扱います。

Chromaticとは:公式UIテスト・レビューSaaS

ChromaticはStorybookチーム公式のUIテスト・レビューSaaSです。Storybookのストーリーをそのままテストとレビューの単位として扱います。

やってくれることは大きく3つです。

  • Publish: storybook build(または旧 build-storybook)の成果物をアップロードし、ビルドごとに公開URLを発行する
  • ビジュアルリグレッションテスト: 前回との見た目の差分(デグレ)をスクリーンショット比較で自動検出する
  • UI Review: ストーリーの特定箇所にピンを立ててコメントし、承認・差し戻しを回す

デザインシステム開発で「Storybookのホスティング先がない」「デザイナーがUIにコメントできる場がない」という2つの課題が同時にあったので、それを一度に解消できるのではという仮説で調査して、導入しました。

ビルドが走ると、こういうサマリーが出ます。

Tests が撮ったテストの総数、Changes が前回から見た目が変わった数です。変わったものはベースライン(次回以降の比較基準)として承認するまで、チェックがpendingのままになります。つまり「勝手に基準が更新される」ことはありません。

StorybookがChromatic上にホスティングされる

ビルドのたびにStorybookがChromatic上にデプロイされ、そのブランチ時点のStorybookを開けるURLが発行されます。PRのbotコメントにも「このブランチの Storybook」というリンクが載るので、クリックするだけで開けます。

つまり、デザイナーやPMは次のことをしなくて済みます。

  • リポジトリをcloneする
  • npm install して npm run storybook を叩く
  • ブランチを切り替えて再起動する

ローカル環境を一切持っていない人でも、PRのリンクから最新のStorybookを見られる状態になります。「Storybookをどこにホスティングするか」という課題が、UIレビューの仕組みとセットで解決するのは大きい点です。

ブランチごとにURLが分かれるので、「mainのStorybook」と「このPRのStorybook」を並べて見比べることもできます。

冒頭に載せたStorybookのスクリーンショットも、Chromatic上にデプロイされたものです。左下に「Back to Chromatic」というボタンが出ていて、そこからレビュー画面に戻れるようになっています。

GitHubのPRに4つのチェックが付く

ChromaticをGitHubリポジトリと連携すると、PRにチェックが付きます。実際に見ると4つありました。

チェック 何を見ているか
Chromatic / Visual test (pull_request) GitHub Actionsのジョブ自体の成否
Storybook Publish Storybookの公開が完了したか(192 stories published)
UI Tests ビジュアル差分。変更をベースラインとして承認するまでpending
UI Review 人によるUIレビューの承認状態

このうち UI Tests と UI Review が、Chromaticならではのゲートになる部分です。

必須にも任意にもできる

これらは普通のGitHub Checksなので、ブランチ保護ルールでrequiredにするかどうかを選べます。

  • 必須にする: デザイナーの承認が下りるまでマージできない。デザインの受け入れをゲートにしたいときはこちら
  • 任意にする: チェックは動いて差分も見られるが、マージはブロックしない。まず様子を見たいときや、UI以外の変更が多いリポジトリではこちら

導入直後は差分とレビューの場としてまず回し始めたかったので、ブロックはさせずに運用しました。「見る場所は作るが、止めはしない」という状態から始められるのが扱いやすいところです。運用が安定してからrequiredへ切り替えれば、移行コストもほぼありません。

botがPRにコメントしてくれる

checks一覧を展開しに行かなくても、ChromaticのbotがPRにコメントを残してくれます。

「何がブロッカーなのか」(ベースライン未承認が85件、未解決の議論が3件)がそのまま書かれているので、ここの Review now から入るのが一番早いです。

進行中は「Testing in progress…」、全部通ると「All tests passed and all changes approved!」のように、同じコメントが状態に応じて書き換わります。

自前のサマリーをPRに出すこともできる

Chromaticのbotコメントにはスナップショットの消費数が出ません。毎回Chromatic側のBuildページを開かないと分からないのですが、Actionsの結果を使えば自前のサマリーをPRにコメントさせることもできます。

上がChromatic公式のbot、下が自前のサマリーです。撮影したスナップショット数とTurboSnapによる再利用数を毎PRに出しておくと、「この変更で何枚使ったか」がその場で分かります。

コメントの末尾に運用ルール(「差分の承認・却下はChromatic上で行い、議論はこのPRに書いてください」)を添えておくのも有効です。レビューの導線がChromaticとPRの2か所に分かれるので、どちらで何をするのかが毎回目に入る状態にしておくと迷いません。

ビジュアル差分は左右に並べて表示される

UI Tests の中身は、前回のビルドとのスクリーンショット比較です。

左が変更前、右が変更後で、差分のあったピクセルが緑でハイライトされます。上の例はサマリーカードに情報(件数や前月比)とアイコンを足した変更ですが、カードの高さが増えたことで下のテーブルが押し下げられ、その部分まで差分として検出されています。

意図した変更であれば承認してベースラインを更新し、意図しない変更であれば差し戻す、という判断をここで行います。

テーマや画面幅を後述のmodesで設定していると、条件ごとに別々のスナップショットが撮られます。

これはダークテーマ・デスクトップ幅(4 dark desktop)の差分です。モードごとに独立したベースラインを持つので、「ライトは問題ないがダークだけ壊れた」「PCは問題ないがモバイルだけ崩れた」を切り分けて検出できます。

デザイナーはピンを立ててコメントする

レビュアーの動きはこうなります。

  1. PRのbotコメント(または UI Review のDetails)からChromaticのレビュー画面を開く
  2. 変更のあったストーリーを順に見ていく
  3. 気になる箇所にピンを立ててコメントする
  4. 問題なければ Accept、直してほしければ Deny

画面上の任意の位置をクリックするとピンが立ち、そこにコメントを書けます。「この切り替えを右寄せにしたいです」のように、座標付きで指摘できるのがポイントです。

コメントボタンが Comment と Comment + deny に分かれているのも地味に便利で、「気になるけど止めるほどではない」指摘と「これは直してほしい」を区別して出せます。

ストーリー単位のコメントも同じように書けます。

コメントは一覧で追える

付いたコメントは Activity タブに溜まります。

どのコンポーネントのどのストーリーに何の指摘が付いているか、サムネイル付きで並ぶので、実装側は上から潰していけます。

直したら返信してResolveする

修正をpushすると新しいビルドが走り、同じ議論スレッドで続きをやり取りできます。

指摘どおり左の線が消えた状態になり、「こちら対応済みです」と返信しています。指摘・修正後の見た目・返信が1か所にまとまるので、SlackとPRとスクショを行き来する必要がありません。

解決したものは Resolve で閉じます。未解決の議論が残っているとPRのチェックがpendingのままになるので、消し込みの状態がそのままマージ可否に効きます。

全部通るとマージできる

ベースラインの承認と議論の解決が終わると、チェックが揃います。

UI Review — Approved by irt-matsuoka、UI Tests — 85 changes accepted as baselines with 1 discussion のように、誰が承認したか・何件をベースラインにしたかがPR上に残ります。

最大のメリットは、デザイナーがコードのdiffを読まなくてよくなることです。「この画面のこのボタンの余白」という言い方でそのまま指摘できるので、スクリーンショットを貼り合わせて説明する手間がなくなりました。

GitHubアカウントがないメンバーにも共有できる

発行URLの公開範囲は、連携先リポジトリの公開設定に自動で同期されます。privateリポジトリならURLもprivate扱いになり、閲覧にはGitHubサインインとリポジトリへのアクセス権が必要です。

GitHubに権限がないデザイナーやPMには、メールやリンクで個別に閲覧権限を招待できます。逆にリポジトリをpublicにすると認証なしで誰でも見られる状態になるので、そこは変更しないよう注意します。

Chromaticの料金はスナップショット数の従量制

Chromaticの料金はスナップショット数(=撮ったスクリーンショットの枚数)で決まります。調査時点のプランはこうでした。

プラン 月額 スナップショット数/月 主な違い
Free $0 5,000 Chromeのみ
Starter $179 35,000 Safari/Firefox/Edge対応、a11yレポート、インタラクションテスト
Pro $399 85,000 カスタムドメイン対応
Enterprise 要問合せ 無制限 SSO、SCIM、SLA

問題は消費量の計算式です。

1ビルドの消費数 = ストーリー数 × テスト対象ブラウザ数 × ビューポート数

掛け算なので、軽い気持ちで設定を足すと一気に増えます。導入したリポジトリは実ストーリー数が約266個あったので、フルビルドすると1ビルドで約266スナップショットです。ここに「PC/モバイルも見たいからビューポート2つ」と足すと532、「Safariも見たい」でさらに倍。この調子だとFreeの5,000枠は十数ビルドで使い切ってしまいます。

実際、検証用に立てたサンドボックス(192ストーリー)で少し試しただけで、この数字になりました。

2,542枚。TurboSnapが0なので全部フルビルドで、13ビルドほどでFree枠の半分を使い切っている計算です。検証中は差分を確認したくて何度も回すので、ここが一番消費しやすいところでした。

注意が要るのは、プランの上限がそのまま1プロジェクトで使える量とは限らないことです。1つのアカウントを複数プロジェクトで共有していれば、枠はその分割られます。上限いっぱいを前提にせず、自分のプロジェクトで収めたい枚数を先に決めておくほうが安全です。

スナップショット消費を抑える5つの設定

1. TurboSnapを有効にする(最も効果が大きい)

Gitの変更差分から影響を受けたコンポーネントだけを撮る機能です。CIのワークフロー(例: GitHub Actions)では、不意の破壊的変更を防ぐため、@latest ではなくメジャーバージョン(例: @v11)を指定することが推奨されます。

- name: Checkout repository
  uses: actions/checkout@v4
  with:
    fetch-depth: 0 # TurboSnapに必須
    ref: ${{ github.event.pull_request.head.sha }} # PRトリガーのエラー回避

- name: Run Chromatic
  uses: chromaui/action@v11 # latestではなくメジャーバージョン指定を推奨
  with:
    projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
    onlyChanged: true

ただし前提条件があります。fetch-depth: 0 が必要で、CIで10回以上の成功ビルド実績が溜まるまでは効きません。条件を満たすまでは、設定してあってもこうなります。

「TurboSnap did not run because it is not yet available for your account」と出て、導入直後の数回はフルビルドで撮られます。ここで枠を使うことは織り込んでおく必要があります。

また pull_request トリガーはデフォルトのmerge commit checkoutだとGit履歴が一致せずエラーになりやすいので、上記のコード例のようにcheckoutステップでheadのshaを明示指定して回避します。

効果は明確に出ました。有効化前は210件のテストに対して撮影222枚・再利用0枚でしたが、有効化後は同規模のビルドでこうなりました。

撮影72枚・再利用138枚。テスト自体は210件走っていますが、実際に撮られたのは72枚だけです。7割近くが再利用に置き換わった計算で、消費の効き方としては他の設定と桁が違います。

Chromatic側のビルド画面でも、TurboSnapのバッジから内訳を確認できます。

ただし、再利用分が枠から完全に消えるわけではありません。Usageページを見ると、再利用されたスナップショットは 1枚あたり0.2枚分としてカウントされていました。

348 x 0.2 = 69.6 という計算で消費枚数に加算されています。それでも5分の1なので効果は大きいのですが、「TurboSnapを入れたから再利用分は0」と見積もると枠の計算がズレます。

2. トリガーを絞る

特に schedule(cron定期実行)は、コードが変わっていなくても撮って消費するので採用しませんでした。

on:
  pull_request:
    branches: [dev]
  push:
    branches: [dev]
  workflow_dispatch:

push はbaseline(差分比較の基準)を更新するために dev だけに絞ります。workflow_dispatch は叩かない限り消費しないので、オンデマンド確認用の保険として付けておいて損はありません。

3. 撮らないストーリーを決める

トークン一覧のように見た目の回帰チェックが重要でないストーリーは、対象外にします。

parameters: { chromatic: { disableSnapshot: true } }

4. 画面幅は必要なストーリーのみ(modes活用)

レスポンシブ確認は魅力的ですが、幅の数だけ消費数が掛かります。全ストーリーには付けず、PCとモバイルの比較が本当に必要なものだけに絞ります。

ここで実装できずに詰まった点があります。幅の指定でよく紹介されている parameters.chromatic.viewports はレガシーAPIで、modesと併用できません。同じストーリーに両方があるとビルドが落ちます。

Error: Chromatic does not support viewports and modes on the same story.

テーマ切り替えでmodesを使っている場合、幅の指定もmodes側に寄せる必要があります。条件(テーマ・幅)の組み合わせをモードとして定義しておく形です。

// .storybook/modes.ts
export const allModes = {
  light: { theme: "light" },
  dark: { theme: "dark" },
  "1 light desktop": { theme: "light", viewport: 1440 },
  "3 light mobile": { theme: "light", viewport: 390 },
} as const;

プロジェクト全体の既定は、preview.tsx で軽くしておきます。

// .storybook/preview.tsx
parameters: {
  chromatic: { modes: { light: allModes.light } },
}

ここで注意が要るのが、modesは上書きではなく「スタック(合成)」されることです。ストーリー側で幅つきモードを足しただけだと、プロジェクト既定の light も残って1枚多く撮られます。幅つきモードだけを撮りたいときは、既定を無効化するオブジェクトを先に展開します。

// .storybook/modes.ts
export const resetModes = {
  light: { disable: true },
  dark: { disable: true },
} as const;
// 各ストーリー
parameters: {
  chromatic: {
    modes: {
      ...resetModes,
      "1 light desktop": allModes["1 light desktop"],
      "3 light mobile": allModes["3 light mobile"],
    },
  },
}

モード名はベースラインのキーそのものなので、あとからリネームすると別モード扱いになり、全部承認し直しになります。最初に決め切ってから運用に乗せたほうが安全です。

なお、Chromaticのビルド画面ではモードが名前順に並び、先頭のモードが最初に開かれます。そのままだと dark desktop が light ... より前に来てダークが既定表示になってしまうので、先頭に番号を振って表示順を固定しています。

5. Draft PRではスキップする

作業中のPRで撮られると無駄なので、Draftのうちは動かさない設定にしています。

スナップショット消費量はどこで確認するか

  • PR単体の消費数: ChromaticのBuild詳細ページ(PRのChecksからリンク)
  • 月間累計: アカウントの Billing / Usage ページ
  • 毎PRで自動的に: Actionsから自前でコメントさせる(前述)

ChromaticのPRページのサマリー(Builds / Discussions / Stories)にも、公式のbotコメントにも消費数は出ません。そこだけ見て安心しないよう気をつけます。毎回Buildページを開くのが面倒だったので、結局は自前のサマリーコメントを出すのが一番確実でした。

まとめ

  • ビルドごとにStorybookがChromatic上にホスティングされ、PRのリンクから開ける。デザイナーがcloneもコマンド実行もせずに最新のUIを確認できる
  • Chromaticを入れると、PRに4つのチェックが付く。ゲートになるのは UI Tests(ベースライン承認)と UI Review(人の承認)で、requiredにするかは選べるので、「止めない」運用から始めて後から締められる
  • デザイナーはChromaticの画面でピンを立ててコメントし、承認状態だけがPRのChecksに返ってくる。指摘・修正後の見た目・返信が1か所にまとまるので、コードのdiffを読まなくてよくなる
  • 料金はスナップショット数の従量制。ストーリー数 × ブラウザ数 × ビューポート数の掛け算なので、ビューポートやブラウザを気軽に足すと危ない
  • TurboSnap(onlyChanged: true)、トリガーの絞り込み、disableSnapshot、画面幅の限定、Draftスキップの5点はほぼ必須の設定。特にTurboSnapは撮影222枚→72枚と効果が桁違い
  • 画面幅の指定は、modesを使っているリポジトリではレガシーAPIの viewports が併用できない(ビルドが落ちる)。modesはスタックするので、既定を打ち消さないと余計な枚数が撮られる
  • 組織で枠を共有している場合は、プランの上限がそのまま自分の使える量ではないので、安全側の目標値を決めておく