クライアントが管理画面からブランドカラーを淡い水色に変更した。その瞬間、ヘッダーに乗っていた白文字がほとんど読めなくなり、後日のアクセシビリティ監査でコントラスト不足の指摘が数十件まとまって返ってきた——テーマ切替やブランドカラー設定の機能を持つサイトを受託で作っていると、この種の事故は珍しくない。色を1つ変えただけで、その色の上に乗る文字・アイコン・ボーダーの可読性が一斉に崩れる。
問題の根は「前景色(テキスト色)を背景色と独立に固定値で持っている」設計にある。--bg: #1a73e8; --text: #ffffff; のように決め打ちしておくと、背景が淡い色に差し替わっても文字色は白のまま固定され、コントラスト比が WCAG の基準を割り込む。テーマやユーザー設定で背景が動くサイトでは、前景色を「背景から自動で導出する」発想に切り替えないと、変更のたびに人手でコントラストを確認し続けることになる。
ここに効くのが CSS の contrast-color() 関数だ。背景色を渡すと、その色に対してコントラストの高い方(白または黒)を返してくれる。本記事では、この関数を軸に「色が変わっても可読性が自動で担保される=自己修正するカラーシステム」を受託案件でどう組むか、そして関数だけでは防げない落とし穴をどう設計で埋めるかを、実装の手順に落として整理する。
固定の前景色をやめ、背景から前景を導出する
まず contrast-color() の素の挙動を押さえる。引数に色を1つ渡すと、白と黒のうち背景に対してコントラストが高い方を返す。両者が同じなら白を返す。つまり「この背景の上に文字を置くなら白と黒どちらが読みやすいか」をブラウザに判定させられる。
.button {
background: var(--brand);
/* ブランドカラーに応じて白か黒かを自動で選ぶ */
color: contrast-color(var(--brand));
}
従来この判定は、Sass のカスタム関数やビルド時スクリプト、あるいは JavaScript のテーマランタイムで輝度を計算してクラスを付け替えていた。--brand が外部から動的に変わるサイトでは JS が必須で、初期描画前の一瞬だけ読めない色が出る、いわゆるちらつきも起きやすかった。contrast-color() は前景色の決定をカスケードの中に閉じ込めるので、背景のカスタムプロパティを上書きするだけで前景が追従する。テーマ切替が「クラス全差し替え+再計算」から「プロパティの上書き1行」に変わるのが本質的な変化だ。
このネイティブCSSへの寄せ方そのものの設計思想は、CSSカスタム関数@functionの記事でトークン計算をビルドからカスケードに移す話として扱っている。contrast-color() はその流れの中で「色の可読性判定」を担当するピースだと捉えると位置づけがはっきりする。
「白か黒の二択」が中間色で破綻する、という前提を持つ
ここで受託として最も重要な注意点を先に出しておく。CSS Color Level 5 の contrast-color() が返せるのは白か黒の二択だけだ。任意のパレットから「読みやすい色」を選んでくれるわけではない(複数候補から選ぶ拡張版は Color Level 6 で議論されている別物で、現時点で実用域ではない)。
そして WCAG の AA 基準(通常テキストで 4.5:1)は、白か黒のどちらを選んでも満たせない中間トーンの背景が現実に存在する。たとえば #2277d3 のような中明度の青の上では、contrast-color() は黒を返すが、小さいテキストでは読みやすいとは言いがたい。MDN も「明るい色か暗い色に対して使うのが推奨」と明記しており、関数は「中間色の背景でも魔法のように読める色を作る」ものではない。
| 背景の明度帯 | contrast-color() の挙動 | 受託での扱い |
|---|---|---|
| 明るい色(淡い背景) | 黒を返す。基準を満たしやすい | そのまま信頼してよい |
| 暗い色(濃い背景) | 白を返す。基準を満たしやすい | そのまま信頼してよい |
| 中間トーン | 白か黒を返すが基準を割ることがある | 関数任せにせず設計で避ける |
つまり「自己修正するカラーシステム」は、contrast-color() を貼れば完成するのではない。前景を自動化する代わりに、背景パレット側を「明るい/暗いに寄せて、危険な中間トーンを最初から作らない」ように設計する。この役割分担が抜けると、関数を入れたのにコントラスト不足が残り、かえって「自動化したから大丈夫」という油断を生む。色の自動化と、危険域を避けるパレット設計はセットだ。
トークン設計に組み込む — 背景を起点に前景を生やす
設計の勘所は、トークンの依存方向をそろえることにある。背景色トークンを「真実の一つの源」にして、前景・ボーダー・アイコン色はそこから導出する。前景を独立トークンとして手で持たないのがポイントだ。
@property --surface {
syntax: '<color>';
inherits: true;
initial-value: #1a73e8;
}
:root {
/* 背景=真実の源。前景はここから導出する */
--surface: #1a73e8;
--on-surface: contrast-color(var(--surface));
}
/* テーマやブランド設定では背景トークンだけを上書きする */
[data-theme='brandlight'] {
--surface: #eaf3ff; /* 前景は触らない。自動で黒に切り替わる */
}
.panel {
background: var(--surface);
color: var(--on-surface);
}
--surface を @property で <color> 型として宣言しておくと、不正な値が入っても宣言ごと無視されて初期値に戻るだけで、他のプロパティを巻き込んで壊れない。テーマやユーザー設定で書き換えるのは原則 --surface 系のトークンだけにして、--on-surface 系は導出に任せる。この一方向の依存を守るだけで、「背景を変えたら文字色も追従する」という自己修正の性質が、トークン基盤の構造として保証される。
中間トーンを避ける設計は、このパレット定義の段階で効かせる。ブランドカラーを管理画面で自由入力させるのではなく、あらかじめ用意した「明るい面」「暗い面」のサーフェス・トークンに割り当てる形にすると、contrast-color() が安全に働く明度帯の中にブランドカラーを収められる。色の自由度を少しだけ削る代わりに、可読性の保証を構造で買う、という設計判断だ。デザインシステム全体としてこの構造をどう持つかは、AI対応デザインシステムの記事で扱ったトークンの一元管理の考え方と地続きになる。
フォールバックを先に書く — 未対応環境で文字を消さない
contrast-color() は2026年4月に主要3エンジン(Chrome 147・Firefox 146・Safari 26.0)が出そろい、Baseline の Newly available に入った。ただし「Newly available」は「最新版では動く」という意味であり、企業システムや法人向けサイトで現実に残る旧バージョンのブラウザでは未対応のことがある。受託では「最新では動く」を理由にフォールバックを省くと、旧環境で前景色が解決されず文字が背景に溶けて消える事故につながる。
フォールバックの基本形は、まず静的な前景色を先に書き、対応ブラウザだけ @supports で上書きする二段構えだ。CSSの後勝ちの性質を使い、未対応ブラウザには素直な固定値を、対応ブラウザには自動値を渡す。
.panel {
background: var(--surface);
/* フォールバック: 未対応ブラウザはこの固定値を使う */
color: #ffffff;
}
/* 対応ブラウザだけ自動値で上書きする */
@supports (color: contrast-color(black)) {
.panel {
color: contrast-color(var(--surface));
}
}
注意したいのは、フォールバックの固定値も「無難に読める色」にしておくこと。ここを白で固定したまま、淡い背景テーマを許すと、未対応ブラウザではまさに冒頭の「白文字が読めない」事故が再現する。フォールバック値は、そのトークンが取りうる背景の範囲で最も無難な側に倒す。前述の「背景を明るい/暗いに寄せる」パレット設計をしておけば、フォールバック値も決めやすくなる。設計とフォールバックが同じ前提を共有しているか、引き渡し前に必ず突き合わせる。
@supports での出し分けや機能検出を前提にネイティブCSSを案件へ取り込む進め方は、CSSアンカーポジショニングの記事でも同じ枠組みで整理している。新しいCSS機能を受託で使うときは、機能そのものより「未対応時にどう劣化させるか」を先に決めるのが共通の作法だ。
受託案件での適用例と、引き渡しで決めること
弊社が支援したある SaaS 型の予約管理サービス(医療・クリニック向け、社名は伏せる)では、契約クリニックごとに管理画面でブランドカラーを設定できる機能があり、設定された色がそのままヘッダーやボタンの背景に反映される作りだった。淡いパステル系を選ぶクリニックが一定数いて、その上の白文字・白アイコンが読めず、利用者からの問い合わせとアクセシビリティ監査の指摘が継続的に発生していた。前景色が固定の白で、背景だけがテナントごとに動いていたのが原因だ。
施策として、テナントが入力したブランドカラーをそのまま背景にせず、明度を判定して用意済みの「明るい面」「暗い面」サーフェス・トークンへ割り当てる中間層を挟み、前景・アイコン・ボーダー色は contrast-color() から導出する構成に組み替えた。未対応ブラウザ向けには、割り当て先のサーフェスに応じた静的な前景値を @supports の外に置いた。結果、テナントがどの色を選んでも前景が自動で追従するようになり、コントラスト起因の問い合わせは大きく減り、監査での当該指摘は解消した。鍵は関数を貼ったことよりも、「自由入力の色を安全な明度帯のトークンに寄せる中間層」を設けたことにあった。
引き渡しの際に顧客と決めておくべきことは、おおむね次の3点に集約される。
| 決めること | 内容 | 確認の観点 |
|---|---|---|
| 対象範囲 | 自動化する前景の種類(テキスト・アイコン・ボーダー) | どこまで関数任せにし、どこを手で持つか |
| 中間色の扱い | 自由入力色を安全な明度帯へ寄せる中間層を持つか | ブランド色の自由度と可読性のどちらを優先するか |
| フォールバック | 未対応ブラウザでの前景値と、対応保証の範囲 | 旧環境での見え方の許容度 |
この3点を曖昧にしたまま「contrast-color() で自動対応します」とだけ伝えると、中間色での破綻や旧環境での消失が後から顕在化し、追加対応で揉める。自動化の便益と、関数の限界を埋める設計コストは、最初にセットで提示するのが受託としての誠実さだと考えている。
まずどこから手をつけるか
色の可読性が崩れるサイトは、たいてい「前景色を背景と独立に固定値で持っている」一点に問題が集中している。最初の一歩としては、いま運用しているサイトでテーマやブランドカラーを実際に何パターンか切り替えてみて、前景色が背景に追従せず固定のままになっている箇所を洗い出すことを勧めたい。そこが自己修正できていない箇所であり、contrast-color() と中間色を避けるパレット設計で置き換える優先順位がそのまま見える。
そのうえで、自由入力の色を扱う機能があるなら、入力値をそのまま背景にせず安全な明度帯のトークンへ寄せる中間層を設計に組み込めるかを検討してほしい。テーマ機能やブランドカラー設定を持つサイトで、コントラスト起因の監査指摘や問い合わせにお困りでしたら、グリームハブのお問い合わせからご相談ください。現行のトークン構成とテーマ切替の仕組みを拝見したうえで、自己修正するカラーシステムへの組み替えとフォールバック設計をご一緒に進めます。
Sources
- Algorithmic Theming Engines: Building Self-Correcting Color Systems With contrast-color() — Smashing Magazine
- contrast-color() CSS function — MDN
- contrast-color() — CSS-Tricks Almanac
- Automated accessible text with contrast-color() — una.im
- [css-color-6] Rename color-contrast()? — w3c/csswg-drafts Issue #7557