颜色选择器 & 转换器

为什么 HSL 的明度会欺骗你的眼睛 — OKLCH 解决了什么

HSL 把第三个轴叫做"lightness(明度)",因此看起来像是感知均匀的,但它本质上只是把经过 gamma 编码的 sRGB 坐标做了一次圆柱形重映射。L 值就是三个 RGB 通道的 (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 对比度不是黑盒,而是十行代码就能实现的公式。首先把每个 sRGB 通道线性化:先除以 255,如果结果小于等于 0.04045 就再除以 12.92,否则计算 ((c + 0.055) / 1.055) 的 2.4 次方。相对亮度(relative luminance)是加权和 0.2126 R + 0.7152 G + 0.0722 B。权重体现了各原色对感知亮度的贡献,这也是绿色占主导、蓝色几乎不起作用的原因。两个颜色之间的对比度是 (L1 + 0.05) / (L2 + 0.05),其中较亮者作为 L1,取值范围从 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 fails

HEX 写法的边角案例:3、4、6、8 位

CSS 接受四种长度的 hex:#RGB、#RGBA、#RRGGBB、#RRGGBBAA。缩写形式的展开方式是每一位数字重复一次,而不是补零:#f0c 展开为 #ff00cc,#f0c8 展开为 #ff00cc88。手动缩写时这个区别很关键 — #abc 是 #aabbcc,绝不是 #0a0b0c。 8 位形式把 alpha 放在末尾。Android(以及旧版 .NET/WPF)使用 alpha 在前的 ARGB,即 #AARRGGBB。于是同一个字符串 #80FF0000 在 Android 上是 50% 不透明度的纯红,在 CSS 里却被解析为 red 0x80、green 0xFF、blue 0x00、alpha 0x00 — 一个完全透明的黄绿色。页面上什么都不显示,也不报任何错,这个 bug 通常要等到有人逐字符 diff 两套代码库才会被发现。在 Android 应用和 Web CSS 之间移植颜色时,务必重排 alpha 字节的位置。 alpha 百分比和 hex 的映射也并不干净。50% 不透明度是 0.5 × 255 = 127.5,四舍五入为 128 = 0x80 — 所以半透明是 #...80,而不是很多人凭直觉写的 #...7F。hex 的 alpha 只有 255 个离散档位,而 rgb(r g b / 50%) 能保留精确比例,这正是设计 token 越来越偏向斜杠语法的原因。 最后,hex 是仅限 sRGB 的写法。不存在广色域 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)  ->  invisible

sRGB 与 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 上过饱和、在办公室显示器上发灰"的 bug 报告,根源是 gamut 转换而不是你的 CSS。还有一些品牌色,尤其是高饱和橙色,完全落在 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 的建议是接近 #121212 的 near-black 表面而不是 #000000,配合降低饱和度的强调色 — 在白底上恰到好处的高饱和品牌色,放到深色表面上会显得刺眼发颤。不要原样复用亮色模式的 token,而是降低 chroma、略微提高明度。 禁用(disabled)状态优先用 alpha 而不是更浅的灰色:38% 不透明度的元素在任何背景上都能被识别为禁用,而硬编码的 #C0C0C0 一旦放在带色调的卡片上就穿帮了。 最后注意舍入。这个转换器和 CSS 一样,把 H 舍入到整数度、S/L 舍入到整数百分比 — 大约 360 万个 HSL 格点对应 1670 万个 RGB 值。因此往返转换可能让某个通道漂移 ±1。保存一种规范格式(通常是 hex)作为唯一事实来源,其余格式从它派生,不要来回反复转换。
最后更新:

关于此工具

颜色转换器在 HEX(#FACC15)、RGB、HSL 之间互相转换。HSL 把色相、饱和度、明度分开,生成调色板、悬停态、暗色变体比直接调 RGB 直观得多。

使用方法

  1. Pick a color in the visual color picker, or type a HEX value directly.
  2. The RGB and HSL fields update live so you can read off any representation.
  3. Edit any individual channel (R, G, B, H, S, or L) to nudge the color along that axis.
  4. Copy the format your code needs — for example, hsl(48, 96%, 53%) for a Tailwind config or #FACC15 for CSS.
  5. 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.