HSL の Lightness が目を欺く理由 — OKLCH は何を解決するか HSL は第 3 軸を「lightness(明度)」と呼ぶため知覚的に均一に見えますが、実体はガンマエンコードされた sRGB 座標を円柱状に再配置しただけのものです。L 値は文字どおり RGB 3 チャンネルの (max + min) / 2 であり、人間の視覚が波長ごとに明るさを異なって知覚する事実をまったく考慮していません。古典的な例: hsl(60, 100%, 50%) は純粋な黄色、hsl(240, 100%, 50%) は純粋な青です。どちらも L = 50% ですが、相対輝度(relative luminance)は 0.928 対 0.072 — 黄色は目には 12 倍以上明るく見えます。
この問題は、HSL の明度を固定して hue だけを回転させてパレットを作った瞬間に噴出します。黄色とシアンのストップは蛍光のように光り、青と紫のストップはほぼ黒に見え、ある色でコントラスト検査に合格した白テキストが隣の色では不合格になります。結局チームは全ストップを手作業で補正することになり、体系的なパレットという目的そのものが崩れます。
現代的な解決策は OKLCH です。CSS Color Module Level 4 で標準化され、2023 年以降すべての主要ブラウザで利用できます(Chrome 111、Safari 15.4、Firefox 113)。OKLCH の L 軸は知覚的に均一で、oklch(70% 0.1 60) と oklch(70% 0.1 240) は実際に同じ明るさに見えます。L と chroma を固定して hue を回転させれば、白/黒とのコントラストが一定に保たれるランプが得られます。単発の微調整には HSL を、パレットに体系性が必要になったら OKLCH を使ってください。
/* Same HSL "lightness", wildly different brightness */
.hsl-yellow { background: hsl(60 100% 50%); } /* luminance 0.928 */
.hsl-blue { background: hsl(240 100% 50%); } /* luminance 0.072 */
/* OKLCH: the L axis is perceptual, so these really match */
.ok-yellow { background: oklch(70% 0.15 100); }
.ok-blue { background: oklch(70% 0.15 260); }
/* Hue-rotated ramp with constant perceived lightness */
:root {
--cat-1: oklch(70% 0.12 30);
--cat-2: oklch(70% 0.12 150);
--cat-3: oklch(70% 0.12 270);
}自分で計算できる WCAG コントラストの数式 WCAG のコントラスト比はブラックボックスではなく、コード 10 行の数式です。まず各 sRGB チャンネルを線形化します: 255 で割り、結果が 0.04045 以下なら 12.92 で割り、そうでなければ ((c + 0.055) / 1.055) の 2.4 乗を計算します。相対輝度(relative luminance)は加重和 0.2126 R + 0.7152 G + 0.0722 B です。この重みは各原色が知覚的な明るさに寄与する度合いを表しており、だから緑が支配的で青はほとんど効きません。2 色間のコントラスト比は、明るい側の輝度を L1 として (L1 + 0.05) / (L2 + 0.05) で計算し、1:1 から 21:1(純白の上の純黒)までの値になります。
覚えておくべきしきい値: 通常の本文テキストは AA レベルで 4.5:1、大きなテキスト(およそ 24px、または 18.66px の太字)と — WCAG 2.1 SC 1.4.11 以降 — 入力欄のボーダー、フォーカスリング、アイコンなどの非テキスト UI は 3:1、AAA は 7:1 です。便利な基準点: 白背景上の #767676 は 4.54:1 で、本文テキストの AA に合格する最も明るいグレーです。どこにでもある #999999 は 2.85:1 しかなく不合格で、実際のアクセシビリティ監査で最も頻繁に指摘されるのが placeholder と無効状態ラベルのグレーです。
注意点が一つ: WCAG 2.x の数式は暗い背景上の明るいテキストを過大評価することが知られており、数値上 4.5:1 を満たすダークモードのテキストが、ライトモードの対応色より実際には読みにくいことがあります。ダークテーマでは 4.5:1 を目標ではなく下限として扱ってください。
function luminance(r, g, b) {
const lin = (c) => {
c /= 255;
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
};
return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
}
function contrast(rgb1, rgb2) {
const L1 = luminance(...rgb1);
const L2 = luminance(...rgb2);
const [hi, lo] = L1 > L2 ? [L1, L2] : [L2, L1];
return (hi + 0.05) / (lo + 0.05);
}
contrast([118, 118, 118], [255, 255, 255]); // 4.54 — #767676 barely passes AA
contrast([153, 153, 153], [255, 255, 255]); // 2.85 — #999999 failsHEX 表記のコーナーケース: 3・4・6・8 桁 CSS は 4 種類の長さの hex を受け付けます: #RGB、#RGBA、#RRGGBB、#RRGGBBAA。短縮形は 0 埋めではなく各桁を 2 回繰り返して展開されます: #f0c は #ff00cc に、#f0c8 は #ff00cc88 になります。手で値を短縮するときにこの違いが効いてきます — #abc は #aabbcc であって、決して #0a0b0c ではありません。
8 桁形式はアルファを末尾に置きます。Android(および旧 .NET/WPF)はアルファが先頭に来る ARGB、つまり #AARRGGBB を使います。そのため同じ文字列 #80FF0000 が Android では不透明度 50% の純赤ですが、CSS では red 0x80、green 0xFF、blue 0x00、alpha 0x00 — 完全に透明な黄緑としてパースされます。何も描画されず、エラーも出ないため、誰かが 2 つのコードベースを文字単位で diff するまでバグが生き残るのが常です。Android アプリと Web CSS の間で色を移植するときは、必ずアルファバイトの位置を並べ替えてください。
アルファのパーセントと hex の対応もきれいではありません。不透明度 50% は 0.5 × 255 = 127.5 で、四捨五入すると 128 = 0x80 — つまり半透明は #...80 であって、直感で書きがちな #...7F ではありません。hex のアルファは 255 段階しかありませんが、rgb(r g b / 50%) は正確な割合を保持します。デザイントークンがスラッシュ構文を好むようになっているのはこのためです。
最後に、hex は sRGB 専用の表記です。広色域(wide-gamut)の hex は存在しません: Display-P3 の色には hex の綴りがそもそもないので、P3 値を hex にエクスポートするツールは、先に黙って sRGB へ gamut マッピングしています。
#f0c -> #ff00cc (digit doubling, not zero padding)
#f0c8 -> #ff00cc88 (4-digit form is RGBA)
50% alpha: 0.5 x 255 = 127.5 -> rounds to 0x80
CSS 50% red: #FF000080 (RRGGBBAA, alpha last)
Android 50% red: #80FF0000 (AARRGGBB, alpha first)
/* Same string, different platform, silent bug: */
CSS #80FF0000 = rgba(128, 255, 0, 0) -> invisiblesRGB vs Display-P3: rgb(255 0 0) が最も赤い赤ではないとき このページのすべての hex と rgb() 値は sRGB の中に存在します — 1996 年に CRT 蛍光体を基準として標準化された色域(gamut)です。ディスプレイはすでに先へ進みました: iPhone 7 以降のすべての iPhone、2016 年以降のすべての MacBook、大半のフラッグシップ Android とプレミアム Windows パネルが Display-P3 をカバーしており、sRGB より約 25% 多くの色を含みます。特に高彩度の赤・オレンジ・緑での差が顕著です。CSS Color 4 はこれを直接指定可能にします: color(display-p3 1 0 0) は P3 画面では rgb(255 0 0) より目に見えて深い赤にレンダリングされ、sRGB 画面では両者とも同じクランプされた値に収束します。
この差は日常のワークフローに巧妙に入り込みます。macOS のスクリーンショットは Display-P3 でタグ付けされるため、スクリーンショットから拾った色は、その色を生成した CSS と数値が一致しません。P3 に設定された Figma ドキュメントはエクスポート時や sRGB ハードウェアでの表示時に色がずれます — 「自分の Mac では彩度過剰なのに会社のモニターではくすんで見える」という古典的なバグ報告の原因は CSS ではなく gamut 変換です。さらに一部のブランドカラー、特に高彩度のオレンジはそもそも sRGB の外にあり、どの hex コードでも忠実に表現できません。
実践的な指針: sRGB 値をベースラインとして維持し、広色域の色は @supports や @media (color-gamut: p3) のチェックの背後に重ねてください。古いブラウザはパースできない宣言をスキップすることを思い出してください — sRGB のフォールバックを先に書き、color(display-p3 ...) のオーバーライドを次の行に書くだけで、優雅なフォールバックがタダで手に入ります。
Hover・Active・Disabled・ダークモード色の導出 HSL に変換する最も日常的な理由は、インタラクション状態の導出です。明度を 8〜10 ポイント動かす方法 — hover はより暗く、active はさらに暗く — は L 40〜60% 付近の中間トーンではうまく機能します。しかし極端な値では破綻します: L 92% で +8 は純白にクリップされ、L 10% 付近では同じ移動が色の性格そのものを変えてしまいます。導出した明度をおよそ 5〜95% の帯域にクランプし、限界に達したら代わりに彩度を動かしてください。
ダークモードは明度の反転ではありません。L を反転すると hue は保たれますが、純黒の面の上に純白のテキストという組み合わせになり、OLED パネルでは halation(にじみ)を起こし、乱視のユーザーには測定可能なレベルで読みにくくなります。Material Design のガイダンスは #000000 ではなく #121212 付近の near-black サーフェスと、彩度を落としたアクセントトーンです — 白背景で適切に見える高彩度のブランドカラーは、暗い面の上では震えるように見えます。ライトモードのトークンをそのまま流用せず、chroma を下げて明度を少し上げてください。
無効(disabled)状態には明るいグレーよりアルファを使いましょう: 不透明度 38% の要素はどんな背景の上でも無効として読み取れますが、ハードコードされた #C0C0C0 は色味のあるカードの上に置かれた瞬間に破綻します。
最後に丸めに注意。このコンバーターは CSS 自体と同様、H を整数の度に、S/L を整数のパーセントに丸めます — 約 360 万個の HSL 格子点に対して RGB は 1,670 万値。そのため往復変換でチャンネルが ±1 ずれることがあります。正とする形式(通常は hex)を 1 つ保存し、残りはそこから導出してください。相互変換の往復を繰り返してはいけません。
最終更新: 2026-04-30
ツールについて カラーコンバーターは HEX(#FACC15)、RGB、HSL の相互変換を行います。HSL は色相・彩度・明度が独立しているため、パレット生成やホバー色・ダークモード対応の派生が RGB より直感的に行えます。
使い方 Pick a color in the visual color picker, or type a HEX value directly. The RGB and HSL fields update live so you can read off any representation. Edit any individual channel (R, G, B, H, S, or L) to nudge the color along that axis. Copy the format your code needs — for example, hsl(48, 96%, 53%) for a Tailwind config or #FACC15 for CSS. Iterate by adjusting L for a hover state or S for a muted variant of the same hue. 主な使用例 Converting a designer-supplied HEX from Figma to the rgba() format for a CSS shadow. Generating hover and active states by adjusting only the lightness channel. Building a Tailwind config palette in HSL so opacity utilities work cleanly. Sampling a brand color and reproducing it in canvas/SVG code that needs RGB. Producing accessibility-friendly variants by tracking lightness contrast. Translating colors between dark mode and light mode by inverting lightness while keeping hue constant. よくある質問 Q. Why does the same HEX look different in two browsers? A. Color profile handling. sRGB is assumed unless a profile is specified, but some browsers and OS-level color management apply additional gamma. The HEX value itself does not change.
Q. When should I prefer HSL over RGB? A. When you need to derive related colors. HSL lets you change brightness or saturation independently of hue, which is impossible with RGB without color theory math.
Q. What about HSV / HSB — are those the same as HSL? A. No. HSV and HSL share hue and saturation but differ on the third axis: HSV uses Value (peak brightness), HSL uses Lightness (midpoint between black and white). Most CSS workflows prefer HSL.
Q. How do I add transparency? A. Use rgba() or hsla() with an alpha between 0 and 1, or modern CSS color-mix / 8-digit hex (#FACC15CC) where supported.