メインコンテンツまでスキップ

デザインシステムとデザイントークン

この章で学ぶこと

ここまでで、ボタンが bg-primary のような色を使っていることを見てきました。この章では、その色がどこから来るのか、そしてなぜハードコードした bg-blue-500 ではなく bg-primary を使うのかを学びます。これがデザインシステムの出発点です。

  • デザインシステムとは何か
  • デザイントークン(色・余白・角丸の「名前付きの値」)
  • プリミティブトークンとセマンティックトークンの違い
  • OKLCH カラーと、Tailwind v4 の @theme でのトークン定義
  • ライト / ダークテーマの仕組み

デザインシステムとは何か

デザインシステムとは、一貫した UI を作るための「決まりごとの集合」です。色・余白・文字サイズ・角丸・コンポーネントの形などを、チームやプロジェクト全体で統一して使えるようにルール化したものです。

なぜ必要なのでしょうか。デザインシステムがないと、こうなりがちです。

デザインシステムが無い場合

ボタンA: bg-blue-500 / ボタンB: bg-blue-600
ボタンC: bg-sky-500 / ボタンD: bg-[#3b82f6]

・「同じ青のはずなのに微妙に違う」
・「ダークモード対応で全部書き直し…」
・「ブランドカラー変更で全ファイル grep…」

色をその場その場で直接指定(ハードコード)すると、似て非なる色が増え、変更のたびにすべての箇所を直すことになります。デザインシステムは、この問題を「意味のある名前」で解決します。

備考

本書は Button を題材にしていますが、ここで学ぶ考え方は、Card・Input・Dialog などすべてのコンポーネントに共通します。Button はその入り口です。

デザイントークンとは

デザイントークンとは、デザインの最小単位(色・余白・角丸など)に名前を付けた値のことです。

ハードコード:bg-blue-500(具体的な青)
↓ 意味のある名前にする
デザイントークン:bg-primary(「主要なアクションの色」)

bg-primary は「主要なアクションに使う色」という意味を表します。実際の色が青か黒かにかかわらず、コンポーネント側は bg-primary と書くだけ。色そのものは 1 箇所(トークンの定義)で管理します。

トークンの 2 階層:プリミティブとセマンティック

デザイントークンには、2 つの段階があります。

① プリミティブトークン(生の値):oklch(0.205 0 0)=「濃いグレー」

② セマンティックトークン(意味):--primary=「主要アクションの色」

③ コンポーネントが参照:bg-primary → <Button>
  • プリミティブトークンoklch(0.205 0 0) のような「生の色の値」。まだ用途は決まっていません。
  • セマンティックトークン--primary(主要アクション)、--destructive(破壊的操作)のように、役割・意味を与えた名前。

コンポーネントはセマンティックトークンだけを参照します。こうすると、「主要な色を変えたい」と思ったときに --primary の値を 1 つ変えるだけで、すべてのボタンに反映されます。

備考

なぜセマンティックが大事なのか。bg-blue-500 と書くと「青」に縛られますが、bg-primary と書けば「主役の色」という意味に縛られます。後で主役の色を緑に変えても、コンポーネントのコードは 1 文字も変えなくて済みます。これがダークモード対応やブランド変更を楽にする鍵です。

OKLCH カラー

shadcn/ui と Tailwind CSS v4 は、色を OKLCH という形式で表します。

--primary: oklch(0.205 0 0);
/* oklch(lightness chroma hue) の 3 値:
lightness(明度)= 0.205(0 = 黒 〜 1 = 白)
chroma(彩度)= 0(0 = 無彩色グレー)
hue(色相)= 0(0〜360:色の種類 赤・青…) */

OKLCH は「明度・彩度・色相」の 3 つで色を表します。#3b82f6 のような 16 進数より、人間の見た目に近い形で明るさを調整できるのが利点です。たとえば「同じ色のまま少しだけ暗く」がしやすく、ライト / ダークの色設計に向いています。

例:

--destructive: oklch(0.577 0.245 27.325);
/* 明度 0.577 / 彩度 0.245 / 色相 27(赤系)→ 警告・削除に使う赤 */

Tailwind v4 でトークンを定義する

環境構築の章src/index.css@import "tailwindcss" と書きました。トークンは、この CSS ファイルに定義します。shadcn/ui の標準的な定義(一部を抜粋)を見てみましょう。

@import "tailwindcss";

/* ① Tailwind のユーティリティ名と CSS 変数を結びつける */
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
}

/* ② ライトモードの実際の色(OKLCH) */
:root {
--radius: 0.625rem;
--background: oklch(1 0 0); /* 白 */
--foreground: oklch(0.145 0 0); /* ほぼ黒 */
--card: oklch(1 0 0); /* カードの背景(白) */
--card-foreground: oklch(0.145 0 0); /* カード上の文字(ほぼ黒) */
--primary: oklch(0.205 0 0); /* 濃いグレー */
--primary-foreground: oklch(0.985 0 0); /* ほぼ白 */
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325); /* 赤 */
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
}

仕組みは 3 段階です。

  1. :root で CSS 変数(--primary など)に実際の OKLCH 値を定義する
  2. @theme inline で、その変数を Tailwind のユーティリティ名(--color-primary)に結びつける
  3. すると bg-primary / text-primary-foreground といったクラスが使えるようになる
:root の --primary → @theme の --color-primary → bg-primary クラス → <Button>
(OKLCH の値) (Tailwind に登録) (コンポーネントで使用)

完成形の章の Button が bg-primary text-primary-foreground と書けていたのは、この定義があるからです。

備考

環境構築の章で紹介した npx shadcn@latest init を実行すると、この :root / @theme のトークン定義が src/index.css に自動で書き込まれます。手で書く必要はありませんが、「何が書かれているか」を理解しておくと、色を調整したいときに迷いません。

name / foreground の命名規約

トークンの色には、名前名前-foreground のペアという規約があります。

トークン用途
--background / --foregroundページの背景 / 標準のテキスト
--primary / --primary-foreground主要なボタン・アクション
--secondary / --secondary-foreground補助的なアクション
--card / --card-foregroundカードなど面の背景 / その上の文字
--muted / --muted-foreground控えめな背景 / 弱いテキスト
--accent / --accent-foregroundホバーやアクセント
--destructive削除・エラーなどの警告色
--border枠線の色
--input入力欄の枠線
--ringフォーカスリングの色

ルールはシンプルです。

  • 名前 … その上に何かを乗せる「背景」の色
  • 名前-foreground … その背景の上に乗る「文字・アイコン」の色

だから Button の defaultbg-primary text-primary-foreground でした。背景に主役の色、その上に読みやすい文字色、という組み合わせです。

備考

--destructive には -foreground のペアがありません。最近の shadcn/ui では、削除ボタンの文字色に text-white を直接使うようになったためです(完成形の章destructive がそうでした)。規約には、こうした例外も少しあります。

ライト / ダークテーマ

トークンの真価は、テーマの切り替えで分かります。同じ変数を .dark でも定義し、値だけを変えます。

:root {
--background: oklch(1 0 0); /* 白 */
--primary: oklch(0.205 0 0); /* 濃いグレー */
}

.dark {
--background: oklch(0.145 0 0); /* ほぼ黒 */
--primary: oklch(0.922 0 0); /* 明るいグレー */
}
ライト(:root) ダーク(.dark)
--primary: oklch(0.205 0 0) oklch(0.922 0 0)
(暗い色) (明るい色)
↓ どちらも同じクラスで参照
<Button className="bg-primary">
→ クラスは一切変えずに、テーマで色が切り替わる

<html><body>dark クラスを付けるかどうかで、bg-primary の実際の色が切り替わります。コンポーネントのクラスは 1 文字も変えません。これがセマンティックトークンの最大の利点です。

これが Button の土台になる

完成形の章で見た Button のスタイルを思い出してください。

variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-white hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
// ...
}

ここに出てくる色は、すべてこの章で定義したセマンティックトークンです。Button は「青」や「グレー」ではなく、primary(主役)・secondary(脇役)・destructive(警告)・accent(強調)という意味でスタイルを参照しています。

だからこの Button は、

  • テーマ(ライト / ダーク)に自動で追従する
  • ブランドカラーを変えても、トークンを変えるだけで全バリアントに反映される
  • 他のコンポーネント(Card・Input…)と色が自然に揃う

という、デザインシステムに沿った振る舞いをします。

備考

hover:bg-primary/90/90 は「不透明度 90%」という意味です。トークンの色をそのまま薄くしてホバー色にしているので、ホバー色を別途定義する必要がありません。これもトークンを使う利点です。

この章のまとめ

  • デザインシステムは、一貫した UI を作るための決まりごとの集合
  • デザイントークンは、色や余白に「意味のある名前」を付けた値
  • プリミティブ(生の値)→ セマンティック(意味)→ コンポーネント、の順で参照する
  • Tailwind v4 では :root / .dark の CSS 変数 + @theme inline でトークンを定義する
  • 色は OKLCH(明度・彩度・色相)で表し、ライト / ダークの調整がしやすい
  • Button が bg-primary などを使えるのは、このトークンが土台にあるから

次章からは、この土台の上で「バリアントをどう宣言的に管理するか」を cva で学んでいきます。