デザインシステムとデザイントークン
この章で学ぶこと
ここまでで、ボタンが 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 段階です。
:rootで CSS 変数(--primaryなど)に実際の OKLCH 値を定義する@theme inlineで、その変数を Tailwind のユーティリティ名(--color-primary)に結びつける- すると
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 の default は bg-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 で学んでいきます。