Advanced (更新: 2026/6/7)

デザインシステムは「ボタン量産」で死ぬ:コンポーネント設計と運用を小さく育てる

デザインシステムの作り方を、コンポーネントの粒度・命名・バリアント設計、Storybookでの仕様化、チーム運用とガバナンスまで。Claude Codeで小さく育てる実践手順。

デザインシステムは「ボタン量産」で死ぬ:コンポーネント設計と運用を小さく育てる

「デザインシステム作ろう」と意気込んだ最初の1週間で、僕はボタンを11種類作りました。

PrimaryButtonPrimaryButtonLargeSubmitButtonSaveButtonDangerButtonOutline……。Figmaのデザインを1枚ずつ見ながら、出てきた見た目をそのまま部品にしていったんです。きれいに動いた。Storybookにも全部並んだ。満足でした。

3週間後、デザイナーがブランドカラーを少し濃くしたいと言いました。その瞬間、11個のボタンを順番に開いて、同じ色を11回書き換える羽目になりました。1個直すたびに「これ、本当に他と統一されてる?」と不安になる。結局1日溶けました。

これは部品の問題じゃありません。設計と運用の問題です。デザインシステムは、ボタンをたくさん作ると死にます。少ない部品を、正しい粒度と名前で持ち、チームで壊さず育てる仕組みがないと、ただの「使われないUIフォルダ」になる。今日はそこを、Claude Codeを横に置きながら、小さく始めて育てる順番で書きます。

この記事の要点

  • デザインシステムの主役はボタンの数ではなく、コンポーネントの粒度・命名・バリアント設計と、チームでの運用ルール。ここが崩れると即破綻する。
  • バリアントは「見た目」で増やさず「意味」で持つ。variant="blue"ではなくvariant="primary"。色や余白の値そのもの(トークン層)はDesign Tokensの実装ガイドに分離して、この記事はその上のコンポーネントと運用に集中する。
  • Storybookは「部品カタログ」ではなく仕様書として使う。Storyにない状態はレビューもテストもできない。
  • Claude Codeには「全部作って」ではなく「Buttonだけ、既存API互換で、Story付きで」と粒度を切って渡す。境界が曖昧だと巨大差分でレビュー不能になる。
  • いきなり全部を整えない。よく使う1部品から始め、PRレビューと最低限のCIで品質を固定し、安全だと分かった範囲を少しずつ広げる。

デザインシステムは「部品集」ではなく約束事

最初に誤解を解きます。デザインシステムは、ボタンやカードを集めたフォルダのことではありません。

僕の感覚では、デザインシステムはチーム全員が守る約束事です。「保存ボタンはこの形」「危険な操作はこの色」「フォームの間隔はこの広さ」。この約束が言葉とコードで固定されているから、誰が画面を作っても揃う。部品はその約束を運ぶ容器にすぎません。

だから作るときの問いも変わります。「どんな部品が要るか」ではなく、まず「何を揃えたいのか、誰が決めるのか、どうやって壊れないようにするのか」。順番を間違えると、僕みたいにボタンを11個作って後悔します。

そしてもうひとつ。色・余白・文字サイズといった「素の値」をどう管理するかは、この記事では深追いしません。そこはトークン層という別レイヤーの話で、Claude CodeでDesign Tokensを実装する実践ガイドに分けてあります。この記事は、トークンができている前提で、その上に乗るコンポーネントと運用に軸足を置きます。

コンポーネントの粒度:迷ったら「分けない」

設計でいちばん事故るのが粒度です。細かく分けすぎても、大きく作りすぎても痛い。

僕がやらかしたのは前者です。「再利用できそう」という理由だけで、ButtonButtonWrapperButtonInnerButtonLabelButtonIconに分解した。結果、1個のボタンを置くのに4つのコンポーネントを理解しないといけなくなり、誰も使わなくなりました。

今の僕の基準はシンプルです。

  • 3回以上、別の画面で同じものが出てきたら部品にする。1〜2回ならその場で書く。
  • propsで分岐が3つを超えそうなら、別コンポーネントへの分割を考える。
  • 「これは何?」と一言で説明できる単位で切る。説明に「と」が2回入ったら分けすぎ、または大きすぎる。

粒度の言葉として、Atomic Design(原子→分子→生物→テンプレート→ページ)を引き合いに出す人は多いです。考え方の地図としては便利ですが、最初から5階層をきっちり作る必要はありません。僕は「単体で意味を持つ部品(Button、Input)」と「それらを組んだ塊(SearchForm、Card)」の2段だけで始めて、必要になってから増やします。

下の表が、僕が部品化を判断するときの目安です。

サイン部品にするその場で書く
同じ見た目の出現回数3回以上1〜2回
内部の状態(loading/error等)状態を持つ持たない静的表示
チームでの呼び名名前で会話に出る名前がまだ無い
デザインでの扱いFigmaでコンポーネント化済み1回限りの装飾

迷ったときは「分けない」。あとから分けるのは簡単ですが、散らばった部品を統合するのは地獄です。

命名:見た目で名づけると、半年後に詰む

ボタンを11個作ったとき、僕は名前を全部「見た目」でつけていました。BlueButtonLargeButtonOutlineButton。これが半年後に効いてきます。

ブランドカラーが青から緑になった瞬間、BlueButtonは嘘になります。でもコードの何百箇所で使われていて、リネームは怖い。だから誰も直さず、緑色のBlueButtonという地雷が残る。

教訓は一つです。名前は「役割」でつける

  • 良い名前:primary(主要な操作)、danger(取り消せない操作)、secondary(補助)、ghost(背景に溶ける)
  • 悪い名前:blueredbigoutline(全部「見た目」)

役割で名づけておけば、青を緑に変えてもprimaryprimaryのまま。意味が変わらないから、コードを触らずに配色だけ差し替えられます。これはトークン層と地続きの話で、primaryという役割名が、トークン側のaction.background.primaryのような意味ベースの値につながっているのが理想です。

コンポーネント名も同じ発想です。SaveButtonのように「特定の用途」を名前に焼き込むと、保存以外で使えなくなる。Buttonvariantと用途を渡す形にして、名前は汎用に保ちます。命名規則はチームで1枚のメモにして、Claude Codeにも読ませておくと、生成されるコードがブレません。

バリアント設計:「掛け算の罠」を設計で潰す

ここがコンポーネント設計の核心です。バリアント(variant)とは、同じ部品の「見た目や振る舞いの種類」のこと。ボタンなら主要・危険・補助、サイズなら小中大、といった軸です。

問題は、軸が増えると組み合わせが掛け算で爆発することです。variant 4種 × size 3種 × loading × disabled……と素朴に考えると、検証すべき状態が数十通りになる。ここを設計でどう潰すかで、運用のラクさが決まります。

僕のやり方は3つです。

  1. 軸を直交させる。variantとsizeとstateは互いに独立にする。「dangerのときだけsizeが効かない」みたいな例外を作らない。例外が1個あるだけで、使う人は全組み合わせを疑い始めます。
  2. 状態はbooleanで持つloadingdisabledはvariantに混ぜず、独立したフラグにする。PrimaryLoadingButtonのような名前は作らない。
  3. デフォルトを必ず決める。何も指定しなくても安全に動く既定値(primary/md)を用意する。これで「とりあえず置く」が壊れません。

この設計を素直にコードへ落とすと、軸が増えても破綻しません。下は、class-variance-authority(CVA、Tailwindのクラスをバリアント単位で管理するライブラリ)を使った、コピペで動くButtonです。

import { forwardRef, type ButtonHTMLAttributes } from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";

// Tailwindのクラス衝突を解決しつつ結合する小さなヘルパー
function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

// バリアントは「軸」ごとに独立して定義する(直交させる)
const buttonVariants = cva(
  [
    "inline-flex items-center justify-center gap-2 rounded-md font-medium",
    "transition-colors focus-visible:outline-none focus-visible:ring-2",
    "focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
  ],
  {
    variants: {
      // 見た目ではなく「役割」で名づける
      variant: {
        primary: "bg-blue-600 text-white hover:bg-blue-700 focus-visible:ring-blue-600",
        secondary: "border border-gray-200 bg-white text-gray-900 hover:bg-gray-50",
        danger: "bg-red-600 text-white hover:bg-red-700 focus-visible:ring-red-600",
        ghost: "text-gray-900 hover:bg-gray-100",
      },
      size: {
        sm: "h-8 px-3 text-sm",
        md: "h-10 px-4 text-sm",
        lg: "h-12 px-6 text-base",
      },
    },
    // 何も指定しなくても安全に動く既定値を必ず置く
    defaultVariants: { variant: "primary", size: "md" },
  }
);

export interface ButtonProps
  extends ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  // 状態はvariantに混ぜず、独立したフラグで持つ
  loading?: boolean;
}

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
  { className, variant, size, loading = false, disabled, children, ...props },
  ref
) {
  return (
    <button
      ref={ref}
      className={cn(buttonVariants({ variant, size }), className)}
      disabled={disabled || loading}
      aria-busy={loading || undefined} // 読み上げソフトに「処理中」を伝える
      {...props}
    >
      {loading ? (
        <span
          aria-hidden="true"
          className="h-4 w-4 animate-spin rounded-full border-2 border-current border-r-transparent"
        />
      ) : null}
      <span>{children}</span>
    </button>
  );
});

色の値を直書きしている部分(bg-blue-600等)は、運用が育ったらトークン由来のCSS変数に置き換えます。その手順はDesign Tokensの実装ガイドにまとめてあるので、ここではコンポーネントの形に集中してください。大事なのは、variantが役割名で、loadingが独立フラグで、既定値があること。この3点が守られていれば、軸を足しても壊れません。

新しいバリアントを足したくなったら、足す前に一拍置きます。僕は「warningを追加したい」と思うたびに、dangerとの違いを一文で言えるか試します。言えなければ、それは新バリアントではなく、既存の使い方の問題です。バリアントは資産であると同時に、増えるほど検証コストになる負債でもあります。

Storybook:カタログではなく「仕様書」として書く

Storybookを「作った部品を眺める場所」だと思っていると、もったいない使い方になります。

僕にとってStorybookは仕様書です。「このボタンには主要・危険・補助・loading・disabledの状態がある」という約束を、動く形で固定する場所。ここに無い状態は、レビューもできないし、後で説明するテストにもかけられません。逆に言えば、Storyに全状態を並べた時点で、その部品の仕様は確定します。

ポイントは2つ。全状態を1つのStoryに並べることと、a11yチェックを最初から有効にすることです。Storybookのa11yアドオンはparameters.a11y.test"error"にすると、アクセシビリティ違反をテスト失敗として扱えます(これは公式の現行APIです)。

import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "./Button";

const meta = {
  title: "Design System/Button",
  component: Button,
  parameters: {
    layout: "centered",
    // a11y違反をCIで失敗にする(現行の公式オプション)
    a11y: { test: "error" },
  },
  argTypes: {
    variant: { control: "select", options: ["primary", "secondary", "danger", "ghost"] },
    size: { control: "select", options: ["sm", "md", "lg"] },
    loading: { control: "boolean" },
    disabled: { control: "boolean" },
  },
} satisfies Meta<typeof Button>;

export default meta;
type Story = StoryObj<typeof meta>;

// 全状態を1画面に並べる「仕様書」Story
export const AllStates: Story = {
  render: () => (
    <div className="flex flex-wrap items-center gap-3">
      <Button variant="primary">保存する</Button>
      <Button variant="secondary">キャンセル</Button>
      <Button variant="danger">削除する</Button>
      <Button variant="ghost">あとで</Button>
      <Button size="sm">Small</Button>
      <Button size="lg">Large</Button>
      <Button disabled>Disabled</Button>
      <Button loading>Loading</Button>
    </div>
  ),
};

Claude Codeにこのファイルを任せるなら、「Buttonの公開propsを読んで、全variant・全size・loading・disabled・キーボードフォーカスが見えるStoryを足して。既存Storyは消さないで」と頼みます。Storybook自体の育て方はClaude CodeでStorybook開発を育てる実践ガイドに詳しいので、カタログ運用やテスト連携を深掘りしたい人はそちらへ。

一貫性をテストで担保する:人の目に頼りすぎない

「最後に僕がレビューすれば揃う」。これは忙しい日に必ず破綻します。一貫性は、機械で測れる部分を機械に任せるのがコツです。

僕がCIに入れているのは2種類です。ひとつはa11yチェック(キーボード操作やコントラストの自動検出)。もうひとつはビジュアルリグレッション(見た目が意図せず変わっていないかをスクリーンショット比較で検出)。下は、Storybookの主要Storyに対してアクセシビリティを検査するPlaywrightのテストです。

import { expect, test } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";

// 仕様書Storyを直接開いて検査する
const storyPaths = [
  "/iframe.html?id=design-system-button--all-states",
];

for (const storyPath of storyPaths) {
  test(`a11y ${storyPath}`, async ({ page }) => {
    await page.goto(`http://127.0.0.1:6006${storyPath}`);

    const results = await new AxeBuilder({ page })
      .withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"])
      .analyze();

    // 違反ゼロを期待。出たらCIで止まる
    expect(results.violations).toEqual([]);
  });
}

ビジュアルテストは最初から全Storyにかけるとノイズで疲れます。アニメーションや日付やランダムIDが入ると毎回差分が出るからです。僕は重要Storyだけに絞り、animations: "disabled"で揺れを止めるところから始めました。アクセシビリティの自動テストは万能ではない点も大事です。ラベル欠落やコントラストは拾えても、「文言が文脈に合うか」「キーボード操作が自然か」は人が見る。自動テストは下限を守る門番で、上限は人が決めます。この境界はアクセシビリティ対応の実践ワークフローで具体的に書いています。

チームでの運用とガバナンス:誰が、何を、どう決めるか

部品ができても、チームで使われ続けなければ意味がありません。ここが運用の話です。

僕が痛感したのは、「誰が変更を承認するか」を決めていないと、デザインシステムはすぐ私物化されるということです。ある人が勝手にprimaryの色を変え、別の人が知らずに別の値に戻す。そんな綱引きが起きます。

最低限、次の3つを文書にします。

  1. オーナーを決める。1人でも委員会でもいい。「バリアント追加・トークン変更はこの人/この場でレビュー」という窓口を1つにする。
  2. 追加のハードルを上げる。新バリアント・新コンポーネントは、既存で代替できないことを一文で説明してからPRを出すルールにする。増やすより減らすほうが難しい資産だからです。
  3. 変更の影響範囲を出すprimaryの色を変えるPRには「どの画面に影響するか」を添える。Claude Codeに「Buttonvariant="primary"を使っている箇所を一覧化して」と頼むと、この棚卸しが速いです。

依頼の粒度も運用の一部です。Claude Codeに渡す範囲を切らないと、レビュー不能な巨大差分が返ってきます。下のような作業ルールをCLAUDE.mdに書いておくと、毎回同じ品質で動きます。

デザインシステム作業ルール:
- 触ってよいのは src/components, src/styles, .storybook, tests のみ。
- バリアントや色を変えるときは、旧名と新名、影響する画面を必ず一覧にする。
- 新コンポーネントには TypeScript props、キーボード操作、Storybook story、a11y メモを必ず含める。
- 完了報告の前に test:storybook と test:a11y を実行する。
- フォーカス挙動に触れる変更は、手動確認の手順を添える。

ガバナンスというと堅苦しいですが、要は「壊す前に一拍置く仕組み」です。小さいチームなら、この5行のルールと窓口1人で十分回ります。

小さく始めて育てる:最初の2週間の順番

最後に、ゼロから始める人向けの順番を置きます。僕がボタン11個で失敗したあと、やり直してうまくいった道です。

  • 1〜2日目:いちばん使う部品を1つだけ選ぶ(たいていButton)。既存の使われ方を全部洗い出し、何種類の見た目があるかを表にする。ここで初めて「本当に必要なバリアント」が見える。
  • 3〜5日目Buttonを役割ベースの命名・直交バリアント・既定値ありで1個だけ実装する。全状態のStoryを書く。この時点で他の部品には手を出さない。
  • 2週目前半:a11yチェックをCIに繋ぐ。PRで自動的に違反が止まる状態を作る。重要Storyのビジュアルテストも1本足す。
  • 2週目後半:運用ルール(オーナー・追加のハードル・影響範囲)を5行で書く。CLAUDE.mdに入れる。ここで初めて2つ目の部品(InputやCard)に進む。

この順番の肝は、1部品で「設計・仕様化・テスト・運用」のフルセットを一周させてから広げることです。10部品を浅く作るより、1部品を深く固めるほうが、結局ぜんぶ速い。テーブルやフォームのような複雑な部品も、この一周ができてから着手すれば崩れません(テーブルの具体はテーブルコンポーネントの実装ガイドにあります)。

最新の仕様やAPIは、Storybook公式ドキュメントで確認しながら進めてください。バージョンで書き方が変わる部分があります。

よくある質問

Q. デザインシステムとコンポーネントライブラリは何が違うんですか? コンポーネントライブラリは部品の集合(コード)です。デザインシステムはそれに加えて、命名規則・使い方のルール・運用の約束・ドキュメントまで含む、もっと広い概念です。部品だけ作ってルールが無いものは、ライブラリではあってもデザインシステムとは呼びにくいです。

Q. 最初からAtomic Designの5階層で作るべき? 不要です。僕は「単体で意味を持つ部品」と「それを組んだ塊」の2段で始めて、必要になってから増やしています。最初から5階層をきっちり設計すると、空のフォルダだけが増えて運用が止まります。考え方の地図として知っておくのは有益ですが、構造として最初から強制しないのがおすすめです。

Q. バリアントはいくつまで持っていい? 数の上限より「一文で他と区別できるか」で判断します。primarysecondarydangerの違いは一言で言える。新しく足すバリアントが既存と一文で区別できないなら、それは増やすサインではなく使い方を見直すサインです。増えるほど検証コストが掛け算で増える点も忘れずに。

Q. デザインシステムの「正本」はFigmaとコードのどっち? チームで先に1つ決めてください。両方を正本にして双方向同期を始めると、どちらも安心して変更できなくなります。実務では「コードを正本にし、Figmaは入力として差分レポートを作る」運用が崩れにくいです。色や余白の値の管理はDesign Tokensの実装ガイドに分けてあります。

Q. Claude Codeにデザインシステムを丸ごと作らせていい? 「全部作って」は失敗します。範囲が曖昧でレビュー不能な巨大差分になるからです。「Buttonだけ、既存API互換で、Story付きで」のように1部品ずつ切って渡し、CLAUDE.mdに作業ルールを置くのがコツです。境界を切るのは人間の仕事、加速は機械の仕事、という分担にすると安定します。

実際に試した結果

ボタン11個で1日溶かしたあと、僕はやり方を逆にしました。部品を増やすのを止めて、Buttonを1個だけ、役割ベースの名前と直交バリアントで作り直した。全状態のStoryを書いて、a11yチェックをCIに繋いだ。

効果はすぐ出ました。次にブランドカラーを変えたとき、触ったのは1箇所だけ。Storyを開けば全状態が一目で見えるから、「これ統一されてる?」という不安が消えました。新メンバーが来ても、Storybookを見せれば「うちのボタンはこの4種類」と5分で説明が終わる。

いちばん変わったのは、僕が「部品を作る人」から「約束を守る仕組みを作る人」になったことです。デザインシステムは、賢い部品をたくさん作る競争じゃない。少ない部品を、正しい名前で持ち、テストと運用ルールで壊れないようにする。遠回りに見えて、これがいちばん長持ちします。まず1部品で一周してみてください。

Claude Codeを使ったコンポーネント設計、Storybook導入、既存UIの棚卸しや運用ルール作りで詰まっているなら、研修・相談ページから相談できます。単発のレビューから、チーム向けのガバナンス設計、実装支援まで対応しています。

#デザインシステム #コンポーネント設計 #Storybook #Claude Code #運用
無料

無料PDF: Claude Code はじめてのチートシート

まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。

スパムは送りません。登録情報は厳重に管理します。

Claude Codeを仕事で使える形にしませんか?

まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。

Masa

この記事を書いた人

Masa

Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。

PR

関連書籍・参考図書

この記事のテーマに関連する書籍を楽天ブックスで探せます。

※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。