OKLCHパレットフォーミュラ
プロダクトのニュートラルカラー(ページ背景、カード表面、フィールド、罫線、4段階のテキスト)は、通常テーマごとに15~30個のhex値が目視で選ばれています。それらは徐々にずれていき、ダークモードはもうひとつの手動選択セットとなり、どのペアがまだコントラストを満たしているのか誰もわかりません。それらを数式に置き換えましょう:
surface-ish token: L = paper + depth × n(token) chroma = tint hue = hue
ink token: L = ink + step × n(token) chroma = ink-tint hue = hue
n(token) はトークンのラダー上の位置で、ラダーはテーマごとに1つです。1つの入力を動かすとランプ全体が連動して動くため、コントラストが生まれるステップ間のギャップは再着色後も維持されます。プロダクトの再着色は、1行(--hue)で完了します。
3つのノブ(--hue、--lightness、--chromaまたは--contrast)がすべての色を駆動します。
ファイル
references/palette.css:スターター入力値とラダーを備えたドロップインルール。scripts/fit_palette.py:既存のhexパレットに入力値をフィッティングし、トークンごとのΔEを出力します。scripts/ladders.json:デフォルトのラダー(ライト、ダーク)。scripts/palette-audit.mjs:実際のブラウザで両テーマのすべてのトークンを解決し、コントラスト違反を報告します。scripts/oklch.py:色変換とΔE/WCAGヘルパー関数。
手順
-
トークンは役割で名前付けし、値では行わない。 デフォルトの例:
- グラウンド:
ground(ページ)、panel(レール、バー)、surface(カード、メニュー)、surface-2(フィールド)、surface-3(チップ、ホバー行)、field。 - ライン:
line、line-2、line-3(罫線、最も薄いものから濃いものへ)、さらにfield-line(コントロールの枠線、3:1を満たす必要あり)。 - インク:
ink、ink-2、ink-3、ink-4(テキスト、最も濃いものから控えめなものへ)。インクステップはすべてテキスト用のため、それぞれ4.5:1を満たす必要があります。
- グラウンド:
-
再選択ではなくフィッティングする。 既存プロダクトでは、
python3 scripts/fit_palette.py theme.cssを実行します。これは--color-*hex宣言(ライト、次に[data-theme="dark"]ルール)を読み取り、デフォルトラダーに対して入力値をフィッティングし、トークンごとのΔEを出力します。ΔEが2未満であれば視覚的に同一とみなせます。 -
デザインがステップ間の間隔を異なるように設定している場合は、色ではなくラダーステップをオーバーライドします。 候補を評価するには、
fit_palette.py theme.css light '<inputs>' '<overrides>'を実行し、最大ΔEが約2以下になるまで調整します。一般的なオーバーライド例:field: 0(フィールドはカード表面と同じ)、より広いline-3、inkとink-2の間のより大きなギャップ。 -
ルールを記述する(
references/palette.cssを参照):- テーマ属性を持つ要素に入力値と
--color-*数式を配置します。 - ダークルールで変更した入力値をすべて再宣言します。キットのデフォルト用に書かれたダークルールは、そうしないと 自身の 値で上書きされます。
- Tailwind v4では、ユーティリティが存在するように
@theme内にトークン名を保持します。実行時の宣言がそれらを上書きします。
- テーマ属性を持つ要素に入力値と
-
実際のブラウザで監査する:
node scripts/palette-audit.mjs <url>。すべてのトークンを描画し、sRGBを読み取り、以下をチェックします:- すべてのインクステップがすべてのグラウンドに対して4.5:1であること。
field-lineとringが3:1であること。
不合格(FAIL)は、そのトークンのラダーステップを移動させることで修正します。色を特殊ケース化してはいけません。既存パレットではFAILが予想されます。17アプリの移行では、監査により半数以上のアプリで
ink-4またはfield-lineの値が不合格となり、それらはすべて以前に出荷されていたものです。
注意点
- カスタムプロパティ内の
var()は、それが宣言された場所で解決されます。- 数式を
:rootに宣言し、子要素に入力値を設定すると、子要素は再色付けされません。 - エイリアストークン(
--color-heading: var(--color-ink))も、同じテーマ要素で宣言する必要があります。
- 数式を
- Tailwind v4の
@theme値は:root上に存在します。 アプルート([data-app])にスコープされたテーマは、@themeではなくその場所で数式を宣言する必要があります。 - canvasや地図ライブラリに色を渡すもの(
getComputedStyle(...).getPropertyValue('--color-x'))は、未解決のoklch(calc(...))文字列を受け取ります。まず解決してください。プローブ要素を描画し、監査と同様の方法で1pxのcanvasから読み取ります。 - グラウンドとインクに同一の色相。 デザインのインクがグラウンドと異なる色相を持つ場合は、
--ink-tintを低く抑えます。Lが0.98の場合、どのクロマも色かぶりとして認識されます。 - ブランド、ステータス、チャートの色は数式の外に維持します。 テキストオンカラーのバリエーション(例:
brand-text)は、別々に選ぶのではなく、同じ色相で固定明度から導出します。 - 混合色相でライト専用のパレットをコンポからサンプリングした場合、ΔE約2以内に収まらない可能性があります。 デザインを歪めるよりはスキップしてください。監査は引き続き適用されます。