チュートリアル

デザイン システムのカラー システムを構築する

6分で読める

アドホック色の選択はカオスに蓄積されます。ブランド青は 1 つのコンポーネントで #3B82F6 として指定され、別のコンポーネントで #2563EB、3 番目の rgba(59, 130, 246, 0.8) - すべておよそ同じ「青」 - 視覚的な矛盾を作成し、ブランド凝集力を弱め、ダーク モードを実装することは信頼できず、すべての色の変更をコードベース考古学プロジェクトに変えます。

カラー システムはこれを解決し、すべてのインターフェース カラーの単一のソースを確立し、2 つのレイヤーに組成されています:利用可能なすべてのカラー値のプリミティブ パレット、および有意なロールに対する対応マッピングのセマンティック トークン レイヤー。この分離はダーク モード、高コントラスト モード、ブランド リスキン構築が可能です - コンポーネント コードのタッチなし。

このガイドは、完全な本番対応カラー システムをゼロから構築することを歩いています:プリミティブ パレットの選択、シェード スケールの生成、セマンティック トークンの定義、CSS と Tailwind での実装、およびドキュメント化とテストの設定。


なぜシステム色が重要なのか

システム色の議論は美的ではなく、操作です。ここは何が破ります:

矛盾は時間とともに複合:プロジェクトの初期段階で、デザイナーと開発者は「正しく見える」アドホック色の決定を行います。2 年以上、コードベースは同じ青であると考えられるが、40 個の異なる青い値に蓄積されます。誰も正しいを知りません。

ダーク モードはセマンティック マッピングが必要:ダーク モードは色が視覚的な値でハードコーディングされている場合、機能できません。color: #1E293B は軽い背景上のテキストとして見える暗い色です。ダーク モードでは、別の値が必要です。80 コンポーネント内で #1E293B をハードコーディングした場合、80 の場所の更新があります - または複雑な CSS オーバーライド システムが新しい技術的負債のレイヤーを作成します。セマンティック トークンはこれを排除します。

テーマの変更は高価です:プライマリ色をブランド変更または変更することは単一の変更であるべきです。セマンティック システムでは、トークン ファイルの --color-primary-600 を変更し、その参照ボタン、リンク、フォーカス リング、および境界線をすべて自動的に更新します。

アクセシビリティ監査は無声失敗:システムなし、すべてのテキスト/背景組み合わせが WCAG 対比要件を渡す確認するには、すべてのテキスト/背景組み合わせを手動で監査する必要があります。セマンティック トークンでは、ルールを定義できます(「text-primary on surface-primary は AA をパス」)、そしてそれらをプログラムで試験してください。


ネーミング規則:プリミティブ 対 セマンティック

整形カラー システムには、異なる目的に果たす 2 つの命名の階層があります。

プリミティブ カラー トークン

プリミティブ トークンはスケール内の位置により名付けられた罰金コロ値です。彼らは使用について仮定をしません - 彼らはただ利用可能な色のパレットです:

blue-50   → #EFF6FF
blue-100  → #DBEAFE
blue-200  → #BFDBFE
blue-300  → #93C5FD
blue-400  → #60A5FA
blue-500  → #3B82F6
blue-600  → #2563EB
blue-700  → #1D4ED8
blue-800  → #1E40AF
blue-900  → #1E3A8A
blue-950  → #172554

プリミティブ トークン名回答:「これはこの色は何ですか?」それらは答えません:「この色は何のための?」

セマンティック カラー トークン

セマンティック トークンはプリミティブ トークンを参照し、その目的により色を名付けます:

color-action-primary       → blue-600
color-action-primary-hover → blue-700
color-action-primary-text  → white-0

color-surface-default      → neutral-50 (light mode) / neutral-900 (dark mode)
color-surface-elevated     → white-0 (light mode) / neutral-800 (dark mode)

color-text-primary         → neutral-900 (light mode) / neutral-50 (dark mode)
color-text-secondary       → neutral-600 (light mode) / neutral-400 (dark mode)
color-text-disabled        → neutral-400 (light mode) / neutral-600 (dark mode)

color-border-default       → neutral-200 (light mode) / neutral-700 (dark mode)
color-border-focused       → blue-500

セマンティック トークン名回答:「この色は何のための?」それらは値を指定しません - プリミティブを参照します。

この分離は重要な構造決定です。コンポーネント参照セマンティック トークン、決してプリミティブ直接。ダーク モードを定義するとき、セマンティック トークンをさまざまなプリミティブに再マップ - コンポーネント コード変更はありません。

Wild ネーミング Schemes

異なるデザイン システムはセマンティック層に異なるネーミング慣例を使用します:

システム セマンティック名の例 規則
Material Design sys.color.primary ドット区切りロール階層
Apple HIG label, secondaryLabel シンプルなロール名
Radix UI --accent-9, --gray-11 スケールベースのセマンティック名
Primer (GitHub) fgColor.default, bgColor.default ロール + バリアント

規則は一貫性より重要。1 つのスキームを選択し全体に適用します。


シェード スケール生成(50-950)

シェード スケールは、単一の色相ファミリ内でニアホワイトからニアブラックへの順序付きシリーズです。Tailwind CSS は 50-950 スケールを採用しました。Material Design 100-900 を使用します。特定の数はガイドラインより背景に戻す非常に軽いティント、インタラクティブ要素の中範囲値、テキストのための深いシェード をカバーするために十分な手順があることができます。

良い色合いスケールを作成するもの

利用可能なシェード スケールは 2 つのプロパティを持ちます:

  1. 十分な知覚ステップ:隣接するシェードは視覚的に異なるべき - 違いが認識されないほど близко、又は隙間が範囲で表示される場合があまり離れています。
  2. 一貫した明るさ進行:明るさ値は 950(暗い)から 50(軽い)への予測可能増加、有無 にすることなく増加すべき驚き、または反転。

ハンドピック色合いスケールの失敗モードは不規則な明るさ間隔です。スケール 300、400、500 が知覚的に似ていますが、500 から 700 へのジャンプは劇的ですが、700 は劇的ですか?有用ではありません - 常に範囲の真中に何か必要とするときに 500 に達します。

OKLCH で生成シェード

OKLCH(OK Lightness-Chroma-Hue)は、その明度チャネルが知覚的に均一であるためシェード生成に最適です - 同じ数値手順は同じ知覚ステップを生成します。L=0.15(非常に暗い)から L=0.98(非常に軽い)の明度値を等しい増分間隔で間隔を置いてください、結果シェード見える視覚的に等ベース間隔されます。

OKLCH で 11 ステップシェード スケール生成:

  1. ベース色相とクロマを選択:H=250(青)、C=0.20。
  2. 明度値を均等に間隔:L = 0.15、0.22、0.30、0.38、0.47、0.55、0.63、0.72、0.82、0.92、0.97。
  3. わずかにクロマを変更:それぞれ削減(非常に軽く、非常に暗い)場所に飽和したが色が適切。
  4. OKLCH 値を実装用の 16 進数に変換します。

青い色相のための実践的な OKLCH-to-hex スケール:

シェード | OKLCH                    | Hex
------+--------------------------+--------
50    | oklch(0.97 0.04 250)     | #EFF6FF
100   | oklch(0.92 0.07 250)     | #DBEAFE
200   | oklch(0.84 0.11 250)     | #BFDBFE
300   | oklch(0.74 0.16 250)     | #93C5FD
400   | oklch(0.65 0.19 250)     | #60A5FA
500   | oklch(0.55 0.22 250)     | #3B82F6
600   | oklch(0.47 0.22 250)     | #2563EB
700   | oklch(0.40 0.20 250)     | #1D4ED8
800   | oklch(0.32 0.18 250)     | #1E40AF
900   | oklch(0.24 0.14 250)     | #1E3A8A
950   | oklch(0.17 0.10 250)     | #172554

ColorFYI の Shade Generator を使用して Tailwind スタイルシェード スケールを任意のベース色から生成し、Color Converter を使用して、生成された各シェードを OKLCH スペースで検査し、明度進行を確認します。

シェード ステップはいくつか必要ですか?

ほとんどのデザイン システムでは、9-11 ステップが適切な範囲です:

  • 50 と 950:表面背景とテキスト/境界線の使用の最も深い値。
  • 100-200:軽いティント背景(アラート背景、白表面上のホバー状態)。
  • 300-400:アイコン色、プレースホルダー テキスト、無効な状態。
  • 500:多くの場合、「純粋な」ブランド色 - ブランド色の基準を指定する 1。
  • 600-700:ホワイト チェック対比が必要な(ボタン、リンク)インタラクティブ要素が満たされます。
  • 800-900:テキストおよびテキスト オン色背景の使用。

パレット内のすべての色相ファミリのシェード生成:プライマリ ブランド色、ニュートラル グレー シリーズ、および任意の強調色(赤/エラー、緑/成功、黄色/警告など)。


セマンティック カラー トークン

プリミティブが確立されたら、プリミティブをロール対応するセマンティック トークンを定義します。このマッピングの粒度が、システムの柔軟性を決定します。

表面色

表面トークンは UI の背景レイヤーを定義します:

:root {
  --color-surface-base: var(--neutral-50);       /* Page background */
  --color-surface-elevated: var(--white);         /* Card, sheet backgrounds */
  --color-surface-overlay: var(--white);          /* Modal, popover backgrounds */
  --color-surface-sunken: var(--neutral-100);     /* Inset fields, code blocks */
}

[data-theme="dark"] {
  --color-surface-base: var(--neutral-950);
  --color-surface-elevated: var(--neutral-900);
  --color-surface-overlay: var(--neutral-800);
  --color-surface-sunken: var(--neutral-950);
}

テキスト色

テキスト トークンはすべてのテキスト ロール階層をカバーします:

:root {
  --color-text-primary: var(--neutral-900);      /* Body text, headings */
  --color-text-secondary: var(--neutral-600);    /* Captions, labels */
  --color-text-tertiary: var(--neutral-400);     /* Placeholder, hint */
  --color-text-disabled: var(--neutral-300);     /* Disabled state */
  --color-text-inverse: var(--white);            /* Text on dark/colored backgrounds */
  --color-text-link: var(--blue-600);
  --color-text-link-hover: var(--blue-700);
}

[data-theme="dark"] {
  --color-text-primary: var(--neutral-50);
  --color-text-secondary: var(--neutral-400);
  --color-text-tertiary: var(--neutral-600);
  --color-text-disabled: var(--neutral-700);
  --color-text-link: var(--blue-400);
  --color-text-link-hover: var(--blue-300);
}

インタラクティブ / アクション色

アクション トークンはインタラクティブ要素のすべての状態をカバーします:

:root {
  /* Primary action */
  --color-action-primary-bg: var(--blue-600);
  --color-action-primary-bg-hover: var(--blue-700);
  --color-action-primary-bg-active: var(--blue-800);
  --color-action-primary-text: var(--white);

  /* Secondary / ghost action */
  --color-action-secondary-border: var(--blue-600);
  --color-action-secondary-text: var(--blue-600);
  --color-action-secondary-bg-hover: var(--blue-50);

  /* Destructive action */
  --color-action-danger-bg: var(--red-600);
  --color-action-danger-bg-hover: var(--red-700);
  --color-action-danger-text: var(--white);
}

フィードバック / ステータス色

ステータス トークンは機能色をセマンティック意味に対応します:

:root {
  /* Success */
  --color-status-success-bg: var(--green-50);
  --color-status-success-border: var(--green-200);
  --color-status-success-text: var(--green-700);
  --color-status-success-icon: var(--green-500);

  /* Warning */
  --color-status-warning-bg: var(--yellow-50);
  --color-status-warning-border: var(--yellow-200);
  --color-status-warning-text: var(--yellow-800);
  --color-status-warning-icon: var(--yellow-500);

  /* Error */
  --color-status-error-bg: var(--red-50);
  --color-status-error-border: var(--red-200);
  --color-status-error-text: var(--red-700);
  --color-status-error-icon: var(--red-500);

  /* Info */
  --color-status-info-bg: var(--blue-50);
  --color-status-info-border: var(--blue-200);
  --color-status-info-text: var(--blue-700);
  --color-status-info-icon: var(--blue-500);
}

ボーダー色

:root {
  --color-border-default: var(--neutral-200);
  --color-border-strong: var(--neutral-400);
  --color-border-focused: var(--blue-500);
  --color-border-error: var(--red-500);
  --color-border-disabled: var(--neutral-200);
}

[data-theme="dark"] {
  --color-border-default: var(--neutral-700);
  --color-border-strong: var(--neutral-500);
  --color-border-focused: var(--blue-400);
  --color-border-error: var(--red-400);
  --color-border-disabled: var(--neutral-700);
}

CSS での実装

CSS 実装は :root レベル(またはテーミングのための [data-theme] セレクター)で定義されたカスタム プロパティを使用します。コンポーネントは、直接プリミティブではなく、セマンティック トークンを参照します。

ファイル構造

design-system/
├── tokens/
│   ├── primitives.css       # All primitive color values
│   ├── semantic.css         # Semantic token → primitive mapping
│   └── dark.css             # Dark mode semantic overrides
└── components/
    ├── button.css
    ├── card.css
    └── ...

primitives.css

:root {
  /* Neutral scale */
  --neutral-50: #F8FAFC;
  --neutral-100: #F1F5F9;
  --neutral-200: #E2E8F0;
  --neutral-300: #CBD5E1;
  --neutral-400: #94A3B8;
  --neutral-500: #64748B;
  --neutral-600: #475569;
  --neutral-700: #334155;
  --neutral-800: #1E293B;
  --neutral-900: #0F172A;
  --neutral-950: #020617;

  /* Blue scale */
  --blue-50: #EFF6FF;
  --blue-100: #DBEAFE;
  --blue-200: #BFDBFE;
  --blue-300: #93C5FD;
  --blue-400: #60A5FA;
  --blue-500: #3B82F6;
  --blue-600: #2563EB;
  --blue-700: #1D4ED8;
  --blue-800: #1E40AF;
  --blue-900: #1E3A8A;
  --blue-950: #172554;

  /* White and black */
  --white: #FFFFFF;
  --black: #000000;
}

コンポーネント使用

コンポーネント参照のみセマンティック トークン:

.button-primary {
  background-color: var(--color-action-primary-bg);
  color: var(--color-action-primary-text);
  border: none;
}

.button-primary:hover {
  background-color: var(--color-action-primary-bg-hover);
}

.button-primary:active {
  background-color: var(--color-action-primary-bg-active);
}

.button-primary:disabled {
  background-color: var(--neutral-200);  /* Exception: disabled state often uses primitives */
  color: var(--color-text-disabled);
}

テーマは軽いから暗いに変わるときに、--color-action-primary-bg は異なる値に解決されます - ボタン固有のダーク モード オーバーライドはありません。


Tailwind CSS での実装

Tailwind v3 および v4 は、config ファイルと CSS カスタム プロパティ アプローチを通じてデザイン トークン統合をサポートします。

Tailwind v3 Config

色スケール tailwind.config.js で定義し、CSS カスタム プロパティにマップ:

// tailwind.config.js
const colors = require("tailwindcss/colors");

module.exports = {
  theme: {
    colors: {
      transparent: "transparent",
      current: "currentColor",
      white: "#ffffff",
      black: "#000000",

      // Primitive scales
      neutral: {
        50:  "#F8FAFC",
        100: "#F1F5F9",
        200: "#E2E8F0",
        300: "#CBD5E1",
        400: "#94A3B8",
        500: "#64748B",
        600: "#475569",
        700: "#334155",
        800: "#1E293B",
        900: "#0F172A",
        950: "#020617",
      },
      blue: {
        50:  "#EFF6FF",
        100: "#DBEAFE",
        200: "#BFDBFE",
        300: "#93C5FD",
        400: "#60A5FA",
        500: "#3B82F6",
        600: "#2563EB",
        700: "#1D4ED8",
        800: "#1E40AF",
        900: "#1E3A8A",
        950: "#172554",
      },

      // Semantic tokens via CSS variables
      "action-primary": "var(--color-action-primary-bg)",
      "action-primary-hover": "var(--color-action-primary-bg-hover)",
      "surface": "var(--color-surface-base)",
      "surface-elevated": "var(--color-surface-elevated)",
      "text-primary": "var(--color-text-primary)",
      "text-secondary": "var(--color-text-secondary)",
      "border-default": "var(--color-border-default)",
    },
  },
};

このセットアップ内では、bg-action-primary を書き込み、正しいセマンティック トークン値に解決され、それは現在のテーマの正しいプリミティブに解決されます。

Tailwind v4 CSS-First Config

Tailwind v4 は JavaScript config を @theme を使用した CSS-first アプローチで置き換えます:

@import "tailwindcss";

@theme {
  /* Primitive colors */
  --color-blue-500: #3B82F6;
  --color-blue-600: #2563EB;
  --color-blue-700: #1D4ED8;
  --color-neutral-50: #F8FAFC;
  --color-neutral-900: #0F172A;

  /* Semantic tokens */
  --color-surface: var(--color-surface-base);
  --color-text: var(--color-text-primary);
  --color-primary: var(--color-action-primary-bg);
}

/* Semantic definitions */
:root {
  --color-surface-base: var(--color-neutral-50);
  --color-text-primary: var(--color-neutral-900);
  --color-action-primary-bg: var(--color-blue-600);
}

[data-theme="dark"] {
  --color-surface-base: oklch(0.145 0 0);
  --color-text-primary: var(--color-neutral-50);
  --color-action-primary-bg: var(--color-blue-500);
}

テストと文書化

カラー システムは、すべてのチームメンバーが理解し、確実に実装されている場合にのみ有用です。テストが退行をキャッチします。ドキュメントはシステムを学習可能にします。

コントラスト テスト

テキスト トークンは、その意図した背景表面トークンとペアで、WCAG 2.1 AA 要件(通常テキスト 4.5:1、大きなテキスト 3:1)を満たすために、プログラムで検証される必要があります。テストを書く:

  1. 現在の CSS カスタム プロパティ値を解決(軽いおよびダーク テーマ両方)。
  2. 各テキスト/表面ペア間の WCAG 対比率を計算します。
  3. 対比比が必要なしきい値を満たす主張します。
import { getContrastRatio } from "some-color-utility";

const tokens = {
  light: {
    textPrimary: "#0F172A",
    surfaceBase: "#F8FAFC",
  },
  dark: {
    textPrimary: "#F8FAFC",
    surfaceBase: "#020617",
  }
};

describe("Color system contrast", () => {
  test("text-primary on surface-base passes AA in light mode", () => {
    const ratio = getContrastRatio(tokens.light.textPrimary, tokens.light.surfaceBase);
    expect(ratio).toBeGreaterThanOrEqual(4.5);
  });

  test("text-primary on surface-base passes AA in dark mode", () => {
    const ratio = getContrastRatio(tokens.dark.textPrimary, tokens.dark.surfaceBase);
    expect(ratio).toBeGreaterThanOrEqual(4.5);
  });
});

ColorFYI の Contrast Checker を使用して、トークン定義中の個別トークン ペアを手動検証してから、自動テストを書きます。

カラー トークン文書化

各セマンティック トークンを文書化: - 名前:正確な CSS 変数名。 - 値(軽い):光モードでそれが参照するプリミティブ。 - 値(暗い):ダーク モード内のプリミティブ。 - 使用:このトークンが使用される場所。 - アンチパターン:それが使用されるべきでない場所。

| Token                        | Light          | Dark           | Usage                          |
|------------------------------|----------------|----------------|--------------------------------|
| --color-text-primary         | neutral-900    | neutral-50     | Body text, headings            |
| --color-text-secondary       | neutral-600    | neutral-400    | Captions, metadata, labels     |
| --color-action-primary-bg    | blue-600       | blue-500       | Primary button background      |
| --color-surface-elevated     | white          | neutral-900    | Card, sheet, panel backgrounds |

ビジュアル回帰テスト

トークン ドキュメントを Percy、Chromatic、または Playwright のスクリーンショット比較などのツールを使用したビジュアル回帰テストとペアにします。ビジュアル回帰テストは、各トークン変更でコンポーネント ライブラリをレンダリングし、スクリーンショットをベースラインに対して比較します - すべての色変更が確認トリガー、回帰をキャッチ。

Storybook カラー パレット ページ

Storybook では、すべてのトークンをスウォッチとしてレンダリングする専用カラー パレット ストーリーを作成します:

// ColorPalette.stories.jsx
const tokens = [
  { name: "--color-surface-base", label: "Surface / Base" },
  { name: "--color-surface-elevated", label: "Surface / Elevated" },
  { name: "--color-text-primary", label: "Text / Primary" },
  // ...
];

export function ColorPaletteDoc() {
  return (
    <div>
      {tokens.map(({ name, label }) => (
        <div key={name} style={{ display: "flex", alignItems: "center", gap: 16 }}>
          <div style={{
            width: 48,
            height: 48,
            backgroundColor: `var(${name})`,
            borderRadius: 4,
            border: "1px solid rgba(0,0,0,0.1)"
          }} />
          <div>
            <div>{label}</div>
            <code>{name}</code>
          </div>
        </div>
      ))}
    </div>
  );
}

これは、実際のライブ トークン値をレンダリングします - Storybook アクティブにあるテーマ内 - 静的ドキュメント ではなく、自己検証されたドキュメント化を作成します。


重要なポイント

  • プリミティブ トークン(スケール位置により名付け、blue-600)をセマンティック トークンから分割してください(目的により名付け、color-action-primary-bg)。コンポーネント常に参照セマンティック、決してプリミティブ。
  • この分離は、セマンティック トークンを異なるプリミティブに CSS カスタム プロパティ レベルでリマップすることで、ダーク モードとテーミングを有効にします。コンポーネント コード変更ゼロ。
  • OKLCH カラー スペース内の生成シェード スケール(50-950)明度チャネルが知覚的に均一 - 同じ数値手順は等しく間隔シェードを生成します。ColorFYI の Shade Generator を使用してアンカー色から Tailwind 互換シェード スケールを作成します。
  • セマンティック トークン カテゴリは 4 つの領域をカバー:表面(背景レイヤー)、テキスト(テキスト役割階層)、アクション(インタラクティブ要素状態)、フィードバック(成功、警告、エラー、情報)。
  • CSS 内でプリミティブを 1 つのファイルで定義し、別のセマンティック マッピング。[data-theme="dark"] セレクター オーバーライドを使用してダーク モードのセマンティック トークンを再マップします。
  • Tailwind v3 内、CSS カスタム プロパティ参照で色オブジェクトを拡張します。Tailwind v4 で、CSS-first @theme ブロックを使用してプリミティブおよびセマンティック トークンを定義します。
  • すべてのテキスト/表面トークン ペア WCAG 対比準拠に対してプログラムで試験 - 手動ではなく。トークン定義中に手動検証するための ColorFYI の Contrast Checker を使用します。
  • Storybook で、実際の CSS カスタム プロパティ値をレンダリングするライブ スウォッチでドキュメント トークン。ドキュメントがシステム実際の現在の状態を反映していることを確認します。

関連カラー

関連ブランド

関連ツール