CSSの相対カラー構文で、元の色の明るさや透明度を変える方法

​​CSS​CSS変数,Playwright

CSS の相対カラー構文を使うと、rgb(from var(--brand) r g b / 50%) のように、元の色をもとに透明度だけを変えた色を作れます。oklch(from var(--brand) calc(l - 0.1) c h) と書けば、色合いはそのままで明るさだけを下げた色になります。元の色の値を手で計算して書き写す必要はなく、色を使う場所で書いておけば、--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)の値です。

書き方は「関数名(from 元の色 チャンネル…)」

相対カラー構文では、色の関数の最初に from と元の色を書きます。そのあとに並べる r g b などの名前は、元の色をその関数の色空間に直したときの、それぞれのチャンネルの値です。

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

.a { background: rgb(from var(--brand) r g b); }          /* 元の色と同じ */
.b { background: rgb(from var(--brand) r g b / 50%); }    /* 透明度だけ 50% にする */
.c { background: oklch(from var(--brand) calc(l - 0.1) c h); } /* 明るさだけ下げる */

使える名前は関数ごとに決まっていて、rgb() なら r g b、hsl() なら h s l、oklch() なら l c h です。どの関数でも、透明度は alpha で受け取れます。元の色は、#1a73d6 のような16進数でも、var() で読んだカスタムプロパティでもかまいません。全体の決まりは、MDN の「相対色の使用」にまとまっています。

透明度を変える

透明度は、/ のあとに書きます。r g b をそのまま並べ、/ 50% だけを足すと、色はそのままで半透明になります。

css
.overlay {
  background: rgb(from var(--brand) r g b / 50%);
}

検証環境では、/ 50% と / 0.5 は同じ色になり、計算値は color(srgb 0.101961 0.45098 0.839216 / 0.5) でした。白い背景の上の画面の色は rgb(140, 185, 234) です。color-mix(in srgb, var(--brand) 50%, transparent) も、計算値と画面の色がこれとまったく同じでした。

元の色がもともと半透明のときは、alpha を使うと、その透明度をもとに計算できます。

css
/* 元の不透明度 0.8 を半分にする */
.soft {
  background: rgb(from rgb(26 115 214 / 0.8) r g b / calc(alpha * 0.5));
}

計算値の不透明度は 0.4 になりました。/ … を省くと、元の色の透明度がそのまま使われ、この例では 0.8 のままでした。

明るさを変えるなら oklch の l を使う

明るさを変えるには、明るさのチャンネルを持つ関数(たとえば oklch() や hsl())に入れて、l を calc() で増やしたり減らしたりします。どちらでも書けますが、変わり方が違います。

css
.darker  { background: oklch(from var(--brand) calc(l - 0.1) c h); }
.lighter { background: oklch(from var(--brand) calc(l + 0.1) c h); }

/* hsl で書く場合 */
.lighter-hsl { background: hsl(from var(--brand) h s calc(l + 10)); }

検証環境で、青(#1a73d6)と黄色(#e0a800)の明るさを、oklch では 0.1 ずつ、hsl では 10 ずつ変えて並べました。色の下の「l」は、できた色を oklch に直したときの明るさの値です。画面に出せる色の範囲(sRGB)に収める前の計算上の値なので、画面の色とは少し違うことがあります。

青と黄色の2色について、oklch で l を変えた5色と、hsl で l を変えた5色を、それぞれ横に並べた図。真ん中が元の色。oklch の段では、明るさの値が 0.1 ずつ等しく変わっている。hsl の段では変わり方がそろっておらず、黄色を明るくしたときは、+10 と +20 の色がほとんど同じ明るさになっている
青(#1a73d6)と黄色(#e0a800)の明るさを、oklch と hsl で段階的に変えた色。数字の下は、できた色の oklch の l(計算上の値)

oklch で変えた色は、明るさの値がどれも 0.1 ずつ変わりました。hsl で変えた色は、変わり方がそろいませんでした。黄色では、+10 の 0.85 から +20 の 0.88 へは 0.03 しか明るくならず、-20 では 0.76 から 0.49 まで一気に暗くなっています。CSS Color 5 の仕様でも、oklch は見た目の差と数値の差がそろうように作られた色空間で、色を操作するのに向いている、と説明されています。ホバーの色のように「少しだけ暗く」したいときは、oklch で l を変えると、元の色が青でも黄色でも、明るさの差を同じ値にそろえられます。

なお、元の色は #1a73d6 のような sRGB の色でも、oklch() に入れれば自動で oklch に直されます。元の色の書き方と、受け取る関数の色空間をそろえる必要はありません。

チャンネルの値は単位のない数になる

calc() で計算するときにつまずきやすいのが、チャンネルの値の単位です。r l などは、元の色を % で書いていても、単位のない数(<number>)として渡されます。数の範囲は関数ごとに決まっています。

関数 名前 渡される数の範囲 例
rgb() r g b 0〜255 calc(r + 50)
hsl() s l 0〜100 calc(l + 10)
oklch() l 0〜1 calc(l + 0.1)
どれでも alpha 0〜1 calc(alpha * 0.5)

検証環境で、rgb(from rgb(10% 50% 90%) r g b) の計算値は color(srgb 0.1 0.5 0.9) で、% で書いた元の色も、そのまま同じ色になりました。範囲は仕様の rgb() の項に「255.0 が 100% に当たる」のように書かれています。

単位を間違えると、次のようになりました。

  • calc(l + 10%) のように、数に % を足すと、色として無効になった(rgb() の r、hsl() と oklch() の l で同じ)
  • oklch(from var(--brand) calc(l + 10) c h) と、hsl の感覚で 10 を足すと、明るさが上限の 1 になり、薄い水色(rgb(173, 255, 255))になった

calc() の外で値を直接書くときは、% も使えます。oklch(from var(--brand) 80% c h) と oklch(from var(--brand) 0.8 c h) は、同じ oklch(0.8 0.171634 255.232) になりました。

範囲を超えた値の扱いは、関数で違いました。oklch の l は、calc(l + 0.6) で 1、calc(l - 0.6) で 0 に収まりました。透明度も 0〜1 に収まりました。一方、rgb(from var(--brand) calc(r + 300) g b) の計算値は color(srgb 1.27843 0.45098 0.839216) で、1 を超えたまま残っていました(画面の色は rgb(255, 115, 214))。

1色から、文字・背景・枠の色を作る

相対カラー構文は、currentColor(その要素の文字色)も元の色にできます。これを使うと、文字色を1つ決めるだけで、背景と枠の色がついてきます。

css
.badge {
  color: oklch(from var(--c) calc(l - 0.15) c h);
  background: rgb(from currentColor r g b / 12%);
  border: 1px solid rgb(from currentColor r g b / 40%);
  border-radius: 999px;
  padding: 4px 14px;
}

.info  { --c: #1a73d6; }
.warn  { --c: #e0a800; }
.error { --c: #d83a52; }

文字は元の色より少し暗くし、背景は文字色を不透明度 12% にした色、枠は 40% にした色です。

「お知らせ」「確認待ち」「エラー」の3つのバッジを、白い背景と薄い水色の背景の上に並べた画面。それぞれ青、暗い黄色、赤の文字に、同じ色の薄い背景と枠が付いている。薄い水色の背景の上では、バッジの背景に下の水色が少し透けている
–c を変えただけの3つのバッジ。背景と枠は、文字色(currentColor)を半透明にした色

検証環境では、currentColor を使った色の計算値は、その要素の文字色で計算された値になっていました。枠を rgb(from currentColor r g b / 30%) にした要素で、文字色を #1a73d6 から #d83a52 に変えると、枠の計算値も color(srgb 0.847059 0.227451 0.321569 / 0.3) と赤の値になりました。

バッジの背景の計算値には、color(srgb -0.110696 0.268924 0.642549 / 0.12) のように、0 を下回る値が入っていました。oklch で暗くした文字色が、sRGB で表せる範囲の外に出ているためです(CSS Color 5 の仕様の例でも、sRGB の値が負になるのは色域の外にあることを示す、と説明されています)。MDN の、sRGB の色域の外にある元の色の説明にも、元の色を別の色空間に直すときに値を範囲内に収めることはしない、と書かれています。上の画像のとおり、バッジは崩れずに表示されました。

カスタムプロパティに入れるときは、使う場所で書く

:root で --brand-soft: rgb(from var(--brand) r g b / 20%) のように作っておくと、子要素で --brand を #d83a52 に変えても、--brand-soft は rgb(from #1a73d6 r g b / 20%) のままで、青の半透明でした。CSS カスタムプロパティの仕様のとおり、var() を置き換えたあとの値が子に受け継がれるためです。color-mix() で作った色と同じ動きです。background: rgb(from var(--brand) r g b / 20%) を、色を使う要素のルールに直接書くと、赤の半透明になりました。

var() と組み合わせて書き間違えると、背景色が透明になる

相対カラー構文を書き間違えたときの動きは、var() を使っているかどうかで変わりました。検証では、試す値の宣言の前に background: red を書いておき、どちらが使われるかを見ました。

書き間違えた値 結果
rgb(from #1a73d6 calc(r + 10%) g b) 前に書いた red が使われた
rgb(from var(--brand) calc(r + 10%) g b) 背景が透明(rgba(0, 0, 0, 0))になった

var() を含まない値は、読み込むときに無効と分かるので、宣言ごと無視されて前の red が残ります。var() を含む値は、置き換えたあとで無効と分かるので、MDN の「無効なカスタムプロパティ」の説明にあるとおり、プロパティの初期値(background-color では透明)になります。チャンネルの数が足りない rgb(from var(--brand) r g) や、rgb() にない名前を使った rgb(from var(--brand) l c h)、定義していない var(--nothing) を元の色にした場合も、同じく透明になりました。color のように継承されるプロパティでは、初期値ではなく親の値(継承値)が使われると、同じ MDN の説明に書かれています。

色が急に消えたときは、まずチャンネルの単位と名前を確かめると早いです。なお、透明度に / 50 のように 1 を超える数を書いたときは、無効にはならず、不透明(1)として扱われました。

透明度だけなら alpha() も使える

透明度だけを変える専用の関数として、CSS Color 5 の仕様には alpha() があります。チャンネルを並べなくてよいぶん、短く書けます。

css
.overlay {
  background: alpha(from var(--brand) / 50%);
}

検証環境の Chromium 153 では、--brand を #1a73d6 にしたこの指定の計算値は rgba(26, 115, 214, 0.5) でした。ただ、互換性データでは Chrome 152、Firefox 155、Safari 27 からの対応で、rgb(from …) よりずっと新しい機能です。

対応しているブラウザー

互換性データでは、相対カラー構文に対応しているバージョンは次のとおりです。

書き方 Chrome・Edge Firefox Safari(iOS を含む)
rgb(from …)、oklch(from …) 122 128 18
hsl(from …) 125 128 18
from currentColor 131 133 18
alpha(from …) 152 155 27
注意

Chrome・Edge 119〜121(hsl() は 119〜124)と Safari 16.4〜17.6 にも、相対カラー構文の古い実装があります。互換性データの注記では、たとえば Chrome 119〜121 の oklch() は l を 0〜100 の数として渡すため、calc(l - 0.1) と書いてもほとんど暗くなりません。これらのバージョンでは、同じ書き方でも違う色になります。

対応していないブラウザーのために書き分けるなら、MDN の対応チェックの説明にあるように、@supports で対応を確かめてから指定を切り替えます。検証環境では、CSS.supports('color', 'rgb(from currentColor r g b)') は true、チャンネルが足りない rgb(from white r g) は false でした。次の CSS を Chromium 153 で開くと、@supports の中の指定が使われました。

css
.overlay {
  background: rgb(26 115 214 / 50%); /* 対応していないブラウザー向け */
}

@supports (color: rgb(from white r g b)) {
  .overlay {
    background: rgb(from var(--brand) r g b / 50%);
  }
}

​Posted by 86