CSSのcolor-mix()で、ホバーや無効のときの色を作る方法

​​CSS​CSS変数,Playwright,アクセシビリティ

ボタンの基本の色を --brand に1つだけ決めておき、ホバーの色は黒を15%、無効の色は白を60%というように、color-mix() で混ぜて作ります。状態ごとに色の値を用意しなくてよく、--brand を書き換えるだけで、すべての状態の色がいっしょに変わります。

補足

検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。ボタンを Playwright で開き、実際にホバーや押下の状態にして、getComputedStyle の値と画面の画素の色を読みました。Firefox と Safari での表示は試していません。各ブラウザーの対応バージョンは、互換性データ(@mdn/browser-compat-data 8.1.8)の値です。

基本の色に黒や白を混ぜて、状態ごとの色を作る

検証に使った CSS です。ボタンの背景は --bg で指定し、状態ごとに --bg だけを変えています。

css
:root {
  --brand: #1a73d6;
}

button {
  --bg: var(--brand);
  background: var(--bg);
  color: #fff;
  border: 0;
  border-radius: 8px;
  padding: 10px 20px;
  cursor: pointer;
}

button:hover {
  --bg: color-mix(in oklch, var(--brand), black 15%);
}

button:active {
  --bg: color-mix(in oklch, var(--brand), black 30%);
}

button:disabled {
  --bg: color-mix(in oklch, var(--brand) 40%, white);
  cursor: not-allowed;
}

.danger {
  --brand: #d83a52;
}

color-mix(in oklch, var(--brand), black 15%) は、--brand を85%、黒を15%の割合で、oklch という色空間で混ぜた色です。片方の割合を省くと、100% からもう片方を引いた値になります。無効の var(--brand) 40%, white は、白が60%です。割合の決まりは、MDN の color-mix() のページにまとまっています。

検証環境でボタンをそれぞれの状態にしたところ、次のように並びました。

2色のボタンを、通常、ホバー、押している間、無効の順に縦に並べた表。左の列は青の「保存する」、右の列は赤の「削除する」。ホバー、押している間と下に行くほど暗くなり、無効は白っぽい薄い色になっている
左は –brand: #1a73d6、右は .danger の中で –brand: #d83a52。どちらも実際にホバー・押下・無効の状態にして撮影

背景色の計算値は、通常のときは rgb(26, 115, 214)、ホバーでは oklch(0.476654 0.145889 255.232) でした。color-mix() で作った色を getComputedStyle で読むと、rgb() ではなく、混ぜた色空間の書き方(ここでは oklch())で返ってきました。

色を変えるときは、–brand だけを書き換える

上の CSS では、.danger の中で --brand を赤に変えただけで、赤のボタンのホバーや無効の色もできています。ホバーの計算値は oklch(0.502598 0.164443 17.1849) で、赤に黒を混ぜた色になりました。

これは、--bg の color-mix() を、button のルールの中に書いているからです。次のように、ホバーの色を :root で作っておくと、.danger の中でも青のままになりました。

css
:root {
  --brand: #1a73d6;
  --brand-hover: color-mix(in oklch, var(--brand), black 15%);
}

.danger {
  --brand: #d83a52;
}

.box {
  background: var(--brand-hover);
}

検証環境で .danger の中の .box を調べると、--brand は #d83a52 になっているのに、--brand-hover は color-mix(in oklch, #1a73d6, black 15%) のままで、背景も青のホバーの色でした。CSS カスタムプロパティの仕様では、カスタムプロパティの計算値は「var() を置き換えたあとの値」で、子要素にはその値が継承されます。:root で var(--brand) が青に置き換わった値が、そのまま子に受け継がれるので、子で --brand を変えても追従しません。

color-mix() は、色を使う要素のルール(ここでは button)の中に書くと、その要素から見た --brand で計算されます。

混ぜる色空間は oklch にした

in のあとに書く色空間によって、同じ15%でも、できる色が変わります。検証環境で、2つの基本の色に黒を15%混ぜ、白い文字とのコントラスト比を比べました。

基本の色 元の色 in srgb で黒を15% in oklch で黒を15%
#1a73d6 4.70 #1662b6(6.08) #135bac(6.73)
#d83a52 4.52 #b83146(5.89) #ae2d40(6.46)

同じ割合でも、oklch で混ぜたほうが暗くなり、白い文字とのコントラスト比が高くなりました。

白を混ぜたときは、srgb と oklch で、色合いの違いがはっきり見て取れました。赤(#d83a52)に白を60%混ぜると、srgb では青みがかったピンクに、oklch では元の赤に近い色合いになりました。oklch の色相の値で比べると、元の色は 17.18 で、oklch で混ぜた色も 17.18 のまま、srgb で混ぜた色は 8.76 にずれていました。黄色(#e0a800)でも、srgb では色相が 84.42 から 91.75 にずれ、緑のほうへ寄りました。

赤と黄色の2行の表。左から元の色、in srgb で白を60%混ぜた色、in oklch で白を60%混ぜた色。赤の行では srgb の色が青みがかったピンク、oklch の色が元の赤に近い淡い色になっている
上は赤(#d83a52)、下は黄色(#e0a800)。白を60%混ぜた色を、srgb と oklch で比べたもの

MDN でも、混ぜたときに灰色っぽくなるのを避けたいときは oklch が向いていて、srgb は暗くなりすぎたり灰色がかったりすることがある、と説明されています。ボタンの状態の色のように、元の色合いのまま明るさだけを変えたいときは、oklch で混ぜると思ったとおりの色になりやすいです。

in を省いて color-mix(var(--brand), black 15%) と書くこともできます。CSS Color 5 の仕様では、省いたときは oklab で混ぜると定められていて、検証環境でも in oklab と同じ色になりました。互換性データには in を省く書き方が別の項目として載っておらず、どのバージョンから使えるかは分かりません。筆者は、どの色空間で混ぜたかがコードから分かるように、in を書いています。

無効の色は、白を混ぜるか transparent を混ぜるか

無効の色は、白を混ぜる代わりに transparent を混ぜて、半透明にする作り方もあります。ここでは srgb で混ぜた色で比べています。

css
/* 白を混ぜる */
button:disabled {
  --bg: color-mix(in srgb, var(--brand) 40%, white);
}

/* transparent を混ぜる */
button:disabled {
  --bg: color-mix(in srgb, var(--brand) 40%, transparent);
}

白い背景の上では、どちらも画面の色は rgb(163, 199, 239) で、見た目は同じでした。opacity: 0.4 を指定したときも同じ色です。違いが出るのは、背景が白でないときです。

白い背景と濃い背景の上に、それぞれ「白を混ぜる」「transparent を混ぜる」の無効のボタンを並べた画面。白い背景の上では2つは同じ薄い青。濃い背景の上では、白を混ぜたほうは薄い青のまま、transparent を混ぜたほうは背景に沈んだ暗い青になっている
上は白い背景、下は濃い背景(#1f2a30)。どちらも –brand: #1a73d6 を40%にして混ぜたもの

濃い背景(#1f2a30)の上では、白を混ぜた色は rgb(163, 199, 239) のまま、transparent を混ぜた色は背景が透けて rgb(28, 71, 114) になりました。transparent を混ぜた色の計算値は color(srgb 0.101961 0.45098 0.839216 / 0.4) で、色は --brand のまま、不透明度だけが 0.4 になっています。

濃い背景の上にも置くボタンで、背景になじむ薄さにしたいなら transparent を混ぜます。どの背景の上でも同じ色にしたいなら、白を混ぜます。

なお、最初の CSS の無効の色(oklch で白を60%混ぜた色)の上に白い文字を載せると、コントラスト比は 1.72 でした。WCAG 2.2 の達成基準 1.4.3 では、文字は4.5:1以上(大きな文字は3:1以上)が求められますが、無効になっている部品の文字にはコントラストの要件がありません。

:hover は :disabled より前に書くか、:not(:disabled) を付ける

button:hover と button:disabled は、どちらも詳細度が 0-1-1 で同じなので、あとに書いたほうが優先されます。最初の CSS では :disabled をあとに書いているので、無効のボタンにホバーしても無効の色のままでした。

順番を入れ替えて :disabled を先に書くと、無効のボタンにホバーしたときに、ホバーの色 oklch(0.476654 0.145889 255.232) に変わってしまいました。押せないボタンなのに、押せそうな色になります。

書く順番に頼りたくないときは、ホバーの指定を無効でないボタンだけにします。

css
button:hover:not(:disabled) {
  --bg: color-mix(in oklch, var(--brand), black 15%);
}

この書き方では、:disabled を先に書いても、無効のボタンにホバーしたときは無効の色のままでした。:not(:disabled) を付けたホバーの指定は、無効のボタンにはそもそも当てはまらないためです。

割合の合計が100%にならないとき

2つの割合は、両方を書いて合計が100%にならなくても使えます。検証環境で確かめた動きは次のとおりです。

書き方 結果
#1a73d6, black(両方省略) 50%ずつ混ぜた色
#1a73d6 80%, black 40%(合計120%) 2:1 の割合に直して混ぜた色
#1a73d6 30%, black 30%(合計60%) 50%ずつ混ぜた色で、不透明度が 0.6
#1a73d6, black 115% 宣言が無効になり、前に書いた background: red が使われた

合計が100%を下回ると、足りない分だけ透明になります。MDN では、足りない割合の transparent を混ぜるのと似た動きと説明されています。割合に書けるのは 0% から 100% までで、115% や -10% と書いた宣言はまるごと無視されました。

対応しているブラウザー

互換性データでは、color-mix() に対応しているバージョンは次のとおりです。

ブラウザー 対応したバージョン
Chrome・Edge 111
Firefox 113
Safari(iOS を含む) 16.2

​Posted by 86