前段时间给博客做改版,主题色从工程图纸蓝换成了青竹色(竹与玉之色,儒家说君子如竹)。这次换色出乎意料地”无痛”:全站几十处用到强调色的地方——链接 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-paper、text-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 的旋转,深色模式是整个阶梯的翻转。我这次改版最直接的收获就是:色彩系统第一次变成了一个能审计、能演算、改起来不心虚的东西。
