2026年8月1日 · 7 分钟阅读

OKLCH:把颜色变成可以推理的参数

OKLCH:把颜色变成可以推理的参数

博客改版换主题色时,我把全站色彩系统迁到了 OKLCH。这篇文章聊聊这个色彩模型对前端到底好在哪:感知均匀意味着什么,以及换色相、调彩度、翻转深色模式为什么都变成了可预测的参数调整。

Kieran Zhang

Kieran Zhang

天行健,君子以自强不息

前段时间给博客做改版,主题色从工程图纸蓝换成了青竹色(竹与玉之色,儒家说君子如竹)。这次换色出乎意料地”无痛”:全站几十处用到强调色的地方——链接 hover、focus ring、导航激活态、目录高亮——一个值改完,视觉层次纹丝不动,明暗两套模式都不用重调。

无痛的原因不是我细心,而是改版时我顺手把整个色彩系统迁到了 OKLCH。这篇文章聊聊这个色彩模型对前端到底好在哪。

#HSL 的”亮度”是骗人的

先说旧世界的问题。CSS 里传统的颜色写法,hex 和 rgb 是给机器看的存储格式,人类不可读;HSL 看起来像是给人用的——色相、饱和度、亮度,三个参数多直观——但它有个根本缺陷:它的亮度轴和人眼感知对不上

hsl(60 100% 50%) 是黄色,hsl(240 100% 50%) 是蓝色,名义上”亮度”都是 50%,实际上黄色刺眼地亮,蓝色深得发黑。因为 HSL 只是对 RGB 立方体做了个几何变形,完全没有考虑人眼对不同波长的敏感度差异。

这个缺陷的实际后果是:HSL 里一切跨色相的操作都不可信。你不能说”这两个颜色 L 一样所以一样亮”,不能靠固定步长生成深浅色阶(中间档会忽明忽暗),更不能换个色相还指望明暗关系保持不变。

OKLCH 解决的就是这一件事:感知均匀。它的三个分量——L(感知亮度,0~1)、C(彩度,0 起无上限)、H(色相角)——是按人眼感知校准的。L 相同的两个颜色,不管什么色相,看起来就是一样亮。

就这一个性质,让下面三件前端日常的事从”碰运气”变成了”改参数”。

#案例一:换主题色,只动 H

改版前的图纸蓝是 hex 时代的遗产:

--color-accent: #1d4ed8; /* 这是什么?多亮?多绿?读不出来 */

迁到 OKLCH 之后,换成青竹色是这样的:

--accent: oklch(0.44 0.075 157); /* 浅色模式 */
--accent: oklch(0.734 0.08 159); /* 深色模式 */

关键在于:确定新色相 157°(竹青)之后,L 是按纸面对比度出来的,不是试出来的。浅色模式纸面 L≈0.98,强调色 L=0.44,对比 7.1:1(AAA);深色模式墨底 L≈0.14,强调色 L 抬到 0.734,对比 8.6:1(AAA)。以后哪天青竹色想换成黛紫色,只要 H 一转、L 不动,所有对比度承诺原样继续成立——这就是”无痛”的来源。

hex 时代做同一件事,是拿着取色器在几十个色值之间逐个肉眼校对。

#案例二:彩度是一根可以精确拨动的旋钮

换色后出了个小问题:青竹色最初定的彩度是 C 0.046,结果它混在正文的墨字里几乎读作灰色,强调色失去了”一眼可辨是绿”的辨识度。

修复就是一行 diff:

- --accent: oklch(0.439 0.046 156.7);
+ --accent: oklch(0.44 0.075 157);

L 和 H 几乎不动,只把 C 从 0.046 拨到 0.075。当时截了三档对比图:C 0.046 沉进墨字读作灰,C≥0.10 开始抢戏,0.075 正好是”可辨但不抢”的门槛。因为动的只有彩度这一个感知维度,亮度没变,所以刚算好的 AAA 对比度完全不受影响。

在 HSL 里没有这种操作——它的 S(饱和度)和 L 是耦合的,调饱和度会连带改变感知亮度,每动一下都要重新检查对比度。

#案例三:深色模式是重映射,不是重绘

全站现在只有九个颜色 token,五个角色:纸、面、墨(分四级浓淡)、线、青竹。墨的四级用的是”墨分浓淡”的思路——黑色加不同透明度:

:root {
  --paper: oklch(0.98 0.006 60); /* 微暖,宣纸 */
  --ink-strong: oklch(0 0 0 / 0.92); /* 标题 */
  --ink: oklch(0 0 0 / 0.87); /* 正文 */
  --ink-secondary: oklch(0 0 0 / 0.6); /* 引用、次要文字 */
  --ink-tertiary: oklch(0 0 0 / 0.38); /* 日期戳 */
}

.dark {
  --paper: oklch(0.141 0.005 286); /* 墨色纸面 */
  --ink-strong: oklch(1 0 0 / 0.92);
  --ink: oklch(1 0 0 / 0.87);
  /* ... */
}

深色模式就是把这列 L 值的阶梯整体翻转:纸从 0.98 压到 0.141,墨从黑翻成白,青竹色 L 从 0.44 抬到 0.734、C 微调防止在暗底上发荧光。因为 L 是感知量,这种系统性变换的结果是可预期的——组件层写 bg-papertext-ink 就完事,全站没有一个 dark: 颜色变体。

反过来看:如果 token 值是 hex,“整体翻转亮度阶梯”这个操作根本无从下手,深色模式只能一个个颜色重新设计。

#顺手的部分

除了上面三件大事,OKLCH 还有些日常的顺手之处:

  • 生态同频。Tailwind v4 的默认调色板已全面改用 OKLCH,自定义 token 和框架生态说的是同一种语言。
  • 相对颜色语法终于有意义了。CSS 现在支持 oklch(from var(--accent) calc(l - 0.08) c h) 这种写法——从一个颜色派生出深一档的变体。这个语法配 HSL 是残废的(因为 L 不可信),配 OKLCH 才真正成立:“减 0.08 的 L”就是视觉上稳定地深一档。
  • 值本身可读。看到 oklch(0.42 0.02 60)(我首页竹影用的暖墨色)能直接读出”中等偏暗、几乎无彩、暖黄相”;#5c554a 读不出任何信息。
  • 广色域的入场券。hex/rgb/hsl 锁死在 sRGB,OKLCH 坐标系本身不限色域。想让青竹色在 P3 屏幕上更”翠”,把 C 往上拨就是了,不用换体系。

#一个要注意的坑

OKLCH 的坐标能写出屏幕显示不了的颜色——比如 L 很高同时 C 很大的组合,在 sRGB 甚至 P3 里都不存在。浏览器遇到这种值会做色域映射(gamut mapping),结果可能和你预期的有偏差。所以定 token 时最好用 oklch.com 这类工具确认值落在目标色域内。我把青竹色 C 拨到 0.075 那次,就是贴着 sRGB 的色域边界在调。

#小结

一句话总结:hex 是存储格式,HSL 是看起来像参数的存储格式,OKLCH 是真正能当设计系统参数用的色彩模型——因为只有它的三个轴和人的眼睛对齐。

对齐之后,颜色就从”设计师的手感”变成了”工程师可以推理的量”:对比度是 L 的减法,色阶是 L 的等差数列,换主题色是 H 的旋转,深色模式是整个阶梯的翻转。我这次改版最直接的收获就是:色彩系统第一次变成了一个能审计、能演算、改起来不心虚的东西。

评论