contrast-color()で自己修正するカラーシステム構築、動的テーマにブラウザネイティブ対比色

contrast-color()で自己修正するカラーシステム構築、動的テーマにブラウザネイティブ対比色

contrast-color()で自己修正するカラーシステム構築、動的テーマにブラウザネイティブ対比色

ウェブ上のカラーコントラスト問題は長らく手つかずだった。HTTP Archiveの調査によれば、2025年時点で約70%のサイトがWCAGの最低限の対比率を満たせていない。WebAIMの2026年データでは、ホームページの83.9%が低コントラストと判定されている。多くの開発者は対比に配慮しているが、実装の手間やランタイム計算の煩雑さが壁になっていた。この状況を根本から変えるCSSの新機能が、

contrast-color()関数だ。背景色を渡すだけで、ブラウザが適切な文字色(黒または白)を計算して返す。JavaScriptやビルドステップは不要で、スタイル計算の段階で解決される。

contrast-color() とは何か

contrast-color() とは何か

基本的な動作

CSS Color Level 5で導入されたこの関数は、引数に与えた色に対して最適な対比を持つblackまたはwhiteを返す。使い方はシンプルだ。

.button {
  background-color: var(--brand-color);
  color: contrast-color(var(--brand-color));
}

--brand-colorを蛍光グリーンに変えれば文字色が黒になり、ネイビーに変えれば白になる。ランタイムのテーマ変更にもリアルタイムで追従する。

手動で固定色を指定(Before)
このテキストは読みにくい
文字色 #333 では背景との対比が不十分
contrast-color() で自動最適化(After)
このテキストはくっきり読める
contrast-color(#3498db) が黒を返し、対比が向上

返り値と名称の変遷

contrast-color()は色値を返すため、border-colorbox-shadowなど色を受け付けるあらゆるプロパティで使える。初期の仕様ドラフトではcolor-contrast()という名前だったが、対比率(数値)を返すように見えるという理由で改名された。古い記事やチュートリアルの構文は現在のブラウザでは動作しないので注意が必要だ。

ブラウザ対応状況

ブラウザ対応状況

Chrome 147、Firefox 146、Safari 26.0のすべての安定版で出荷済みだ。2026年4月にはBaseline Newly Availableステータスを獲得し、主要エンジン間で実装が揃った。Web Platform Testsもパスしており、エッジケースの挙動も統一されている。

グローバルサポート率は一見低く見えるが、更新しないエンタープライズ環境が大半を占める。実際に最新ブラウザを使っている読者なら、ほぼ確実に利用できる。

/* プログレッシブエンハンスメント */
.card {
  background: var(--bg);
  color: #fff;
  text-shadow: 0 0 4px rgb(0 0 0 / 0.8);
}

@supports (color: contrast-color(red)) {
  .card {
    color: contrast-color(var(--bg));
    text-shadow: none;
  }
}

@supportsで未対応ブラウザには影つきの白文字をフォールバックとして提供できる。ただし自動アクセシビリティチェッカー(Lighthouseなど)はtext-shadowを評価せず、フォールバック側をコントラスト違反と誤判定する点は把握しておきたい。

実践的な使い方

実践的な使い方

コンポーネントのベースカラーに

ボタンやカードなど、背景色が変わるUI部品であれば、contrast-color()を一行加えるだけで文字色が自動調整される。

.btn {
  background-color: var(--accent);
  color: contrast-color(var(--accent));
  border: 1px solid contrast-color(var(--accent));
}

複合的なカラー生成との統合

単に黒か白を返すだけでは味気ない場合、他のCSSカラー関数と組み合わせることで表現の幅が広がる。

/* 背景色の色相を取り入れたテキスト */
.card {
  --bg-hue: 260;
  --bg: oklch(0.6 0.1 var(--bg-hue));
  background: var(--bg);
  color: oklch(from contrast-color(var(--bg)) l 0.05 var(--bg-hue));
}

contrast-color()の出力の明度を維持しつつ、少しだけ彩度と背景の色相を加えることで、単なる黒や白ではない深みのある文字色になる。ただし対比が落ちる可能性があるため、最終的な色はアクセシビリティチェッカーで確認しよう。

/* color-mix でソフトな対比を実現 */
.alert {
  --bg: var(--alert-color);
  background: var(--bg);
  color: color-mix(in oklch, contrast-color(var(--bg)) 80%, var(--bg));
  border: 1px solid color-mix(in oklch, contrast-color(var(--bg)) 40%, var(--bg));
}
ベタ黒のテキスト(Before)
警告:この操作は元に戻せません
コントラストは十分だが、やや硬い印象
color-mix で80%混ぜた深みのある色(After)
警告:この操作は元に戻せません
背景の赤を僅かに含んだブラック系で、視認性を保ちつつ柔らかさが増す

上記デモのAfterで使われている色#2E0F0Cは、color-mix(in oklch, black 80%, #e74c3c)を簡易的に再現したものだ。実際のコードではブラウザが動的に最適な中間色を生成してくれる。

light-dark() との連携

システムのカラースキーム(ライト/ダーク)に対応する場合、light-dark()と組み合わせるだけで、OSの設定に応じた対比色が自動的に決まる。

:root {
  color-scheme: light dark;
  --surface: light-dark(#fff, #121212);
}

.component {
  background: var(--surface);
  color: contrast-color(var(--surface));
}

知っておくべき注意点

知っておくべき注意点

トランジションでスナップする

背景色をアニメーションさせると、contrast-color()の返す黒か白の値は離散的なため、スムーズに補間されずに切り替わる。しかも切り替えタイミングはWCAG 2.xの相対輝度の特性上、アニメーションの終盤に偏る。

t=0%(開始)
背景が白のとき、文字は黒
t=50%(途中・まだ黒のまま)
背景が中間グレーでも、計算上の閾値は暗い方に偏っているため文字色は黒のまま
t=100%(完了・突然白に切り替わる)
背景が黒になると、一瞬で文字色が白になる

このアニメーションは実際には約1秒かけて連続的に行われるが、文字色だけは終盤でカットインするように変わる。transition-behavior: allow-discreteを使っても、切り替えのタイミングが50%地点にずれるだけで、根本的なジャンプは解消されない。スムーズにしたい場合はcolor-mix()で中間色を手動管理する必要がある。

完全中立のグレーでは白が優先される

両方の対比率がまったく同じになる完全な中間グレー(およそ相対輝度17.9%)では、仕様上白が選ばれる。グレースケールパレットを扱う際に頭の片隅に入れておけば混乱しない。

透明色やグラデーションには使えない

引数は単一の不透明な色に限られる。半透明の色を渡すと、ブラウザが不透明なキャンバス(通常は白)に合成した上で計算するため、意図しない結果になることもある。グラデーションや画像のURLを渡すとパースエラーになる。

従来のアプローチが不要になる

従来のアプローチが不要になる

これまで開発者は、Sassのlightness()関数でコンパイル時に判定したり、--r --g --bチャンネルを分割してcalc()内で輝度計算を行ったりと、複雑なハックで対比色を実現してきた。chroma-jsやpolishedといったライブラリも広く使われてきたが、いずれもランタイムにメインスレッドで計算が走り、SSR時のハイドレーションフラッシュの問題も抱えていた。

contrast-color()はこれらすべてをネイティブのスタイル計算フェーズに置き換える。テーマが変わっても、JavaScriptが走る前から正しい文字色が描画される。対比の自動化は、ケアすることのハードルを限りなくゼロに近づける。

この記事のポイント

  • contrast-color()はCSS Color Level 5で導入され、背景色に応じて黒か白を返す
  • Chrome 147、Firefox 146、Safari 26で出荷済み。主要ブラウザすべてで使える
  • 動的テーマでもJavaScript不要。スタイル計算時に即座に反映される
  • トランジション中はスナップする点や、透明色・グラデーション非対応など注意が必要
  • 他のCSSカラー関数と組み合わせることで、より高度なカラーシステムを構築できる
海田 洋祐

・ 複数業界における17年間のデジタルビジネス開発経験 ・ ウェブサイト開発のためのHTML、PHP、CSS、Java等の実用的知識 ・ 15ヶ国語対応の多言語SaaSの開発経験 ・ 17年間にも及ぶ、Eコマース長期運営経験 ・ 幅広い業界でのSEO最適化の豊富な経験

メッセージを残す