カラートークンパターン
問題
Tailwind CSS はデフォルトで約22のカラーファミリーを持ち、各ファミリーに11のシェード(50〜950)があるため、240以上のカラーユーティリティがあります。実際には、あるコンポーネントで blue-500 を使い、別のコンポーネントで blue-600 を使い、3つ目で indigo-500 を使う — すべて「プライマリボタンの青」のつもりです。制約がなければすべてのシェードが等しく有効なので、不整合は静かに広がっていきます。
同じずれはグレーでも起きます。ある開発者はカード背景に gray-100 を使い、別の開発者は slate-50 を選び、3人目は zinc-200 を使います。すべて「明るい背景」ですが、どれも一致しません。時間が経つにつれて、UIは視覚的な統一感を損なう微妙に異なるトーンのパッチワークになっていきます。
解決方法
すべてのデフォルトカラーをリセットし、目的別に整理された小さなセマンティックカラートークンのセットを定義します。リセット後、bg-blue-500 や text-gray-700 はもう動作しません — チームはプロジェクトの意図的なカラー語彙を使うことを強制されます。
トークンカテゴリ
以下のカテゴリはセマンティックレイヤリングのアプローチに従っています — 生のパレット値を、コンポーネントが参照するロールベースのトークンに置き換えます。これは Three-Tier Color Strategy(パレット → テーマ → コンポーネント)と同じ原則を、Tailwind の @theme システムに特化して適用したものです。
カラーを5つのグループに整理します:
- ブランドカラー —
primary、secondary、accent、それぞれにlight、base、darkのバリアント - セマンティック/ステートカラー — フィードバックやステータス用の
success、warning、error、info - サーフェスカラー — 背景用の
surface、surface-alt、surface-inverse - テキストカラー — 読みやすい階層のための
text、text-muted、text-inverse - ボーダーカラー — エッジやフォーカスリング用の
border、border-focus
@theme カラーブロック
@theme {
/* Reset ALL default colors */
--color-*: initial;
/* ── Brand ── */
--color-primary-light: hsl(217 91% 60%);
--color-primary: hsl(221 83% 53%);
--color-primary-dark: hsl(224 76% 48%);
--color-secondary-light: hsl(250 80% 68%);
--color-secondary: hsl(252 78% 60%);
--color-secondary-dark: hsl(255 70% 52%);
--color-accent-light: hsl(38 95% 64%);
--color-accent: hsl(33 95% 54%);
--color-accent-dark: hsl(28 90% 46%);
/* ── State ── */
--color-success: hsl(142 71% 45%);
--color-warning: hsl(38 92% 50%);
--color-error: hsl(0 84% 60%);
--color-info: hsl(199 89% 48%);
/* ── Surface ── */
--color-surface: hsl(0 0% 100%);
--color-surface-alt: hsl(210 40% 96%);
--color-surface-inverse: hsl(222 47% 11%);
/* ── Text ── */
--color-text: hsl(222 47% 11%);
--color-text-muted: hsl(215 16% 47%);
--color-text-inverse: hsl(210 40% 98%);
/* ── Border ── */
--color-border: hsl(214 32% 91%);
--color-border-focus: hsl(221 83% 53%);
}
この設定後、bg-surface、text-primary、border-border-focus などの Tailwind ユーティリティだけが利用可能なカラーオプションになります。bg-gray-100 を使おうとするとビルドエラーになります。
デモ
デフォルトグレー vs セマンティックサーフェストークン
左側は Tailwind の22種類のデフォルトグレーシェードのサンプルです — すべて背景として技術的に有効です。右側はそれらを置き換える3つのセマンティックサーフェストークンです。選択肢が少ないほど、決定が速くなり、一貫性が保証されます。
セマンティックカラートークンを使ったボタンセット
これらのボタンはセマンティックカラートークンのみを使用しています — primary、secondary、error、accent。数値カラーシェードは一切使われていません。プロジェクト内のすべてのボタンがこれらのトークンを使うため、パレットは一貫性を保ちます。
サーフェス、テキスト、ボーダートークンを使ったカード
このカードは5つのトークンカテゴリすべてが連携して動作することを示しています。背景には surface と surface-alt を、テキストには text と text-muted を、ボーダーには border を、バッジには primary ブランドカラーを使用しています。コンポーネント内のすべてのカラーが正確に1つのセマンティックトークンに対応しています。
パレット拡張の命名規則
タイトトークンセットでプロジェクトを始めると、各カラーファミリーは通常1つの値しか持ちません。最初の名前はシンプルに — gray であって gray1 ではありません。後でそのファミリーに2つ目のシェードが必要になったら、gray2 を追加します。3つ目は gray3 になります。
@theme {
/* ── Initial palette ── */
--color-gray: hsl(25 5% 45%);
/* ── Added later when a dark card surface was needed ── */
--color-gray2: hsl(0 3% 13%);
}
この「最初は番号なし」ルールにより、初期のトークン名がクリーンに保たれ、パレットが拡張される際のリネームの連鎖を防ぎます:
gray→ 初日から使われているオリジナルのグレーgray2→ ダーク背景のために後から追加gray3→ ミュートなボーダーのためにさらに後から追加
1から番号を振る方式(gray1、gray2、gray3)と比較すると、最初のトークンに無意味なサフィックスが付き、後から gray1 を遡って挿入しようとすると既存の参照すべてを更新する必要があります。
このパターンはすべてのカラーファミリーに適用されます:primary / primary2、surface / surface2、accent / accent2 など。
実例
zmod プロジェクトではまさにこのパターンを使っています:
--zd-color-gray: rgb(120, 113, 108); /* Original gray */
--zd-color-gray2: #201f1f; /* Added later for dark backgrounds */
gray2 が追加された際にリネームは必要ありませんでした — オリジナルの gray はコードベース全体でそのまま残りました。
ビフォー・アフター:パレットの拡張
このデモは、最初は単一の gray トークンを使ったシンプルなカードUIを示しています。デザインが後からダークなカードサーフェスを必要としたとき、オリジナルの gray に触れることなく gray2 が追加されます。
なぜ1から番号を振らないのか
gray1 から始めるのは対称的に見えますが、問題が生じます:
- 最も一般的なトークンへのビジュアルノイズ — 参照の大多数が最初のカラーを使います。
gray1は至る所に無意味な数字を追加します。 - リネームの圧力 —
grayで始めて後で「整理」が必要になった場合、一貫性のためにgray1にリネームしたくなるかもしれません。それはすべてのファイルに影響します。「最初は番号なし」ルールはその圧力を完全に排除します。 - 意図の伝達 —
gray2は「これはオリジナルと並んで追加された2番目のグレーです」と明確に伝えます。gray1/gray2では、両方とも最初から計画されていたように見えます。
使い分け
このカラートークン戦略は、親記事のスペーシングトークン戦略と組み合わせて使うと最も効果的です。合わせることで、視覚的なずれの最も一般的な2つの原因 — スペーシングとカラー — を小さく意図的なデザイン語彙に制約できます。
カラートークンを適用すべきケース:
- プロジェクトに複数の開発者がカラーの選択を行っている場合
- デザインシステムがhex値ではなく名前付きカラー(例:「primary」「surface」)を指定している場合
- ブランドに厳格なカラーガイドラインがあり、一貫して適用する必要がある場合