OKLCH:让「调亮一点」真的只是调亮一点
interactive · css color level 4 · 8 sections
HSL 里把色相从蓝转到黄,亮度数字没变,眼睛看到的亮度却差了一倍。OKLCH 修好的就是这件事——它的三个轴对应人眼实际感知到的三件事,所以你改一个轴,只有那一件事会变。
这份手册按「先看见问题 → 再理解坐标 → 再落到 CSS」的顺序排。每一节的演示都是活的,可以拖;文中所有数值都由 Oklab 官方转换矩阵实算得出,包括几个和流传说法不一致的地方,我在正文里标了出来。
01先看见问题
同一个亮度数字,两种亮度
下面两条色带,色相都从 0° 扫到 360°,亮度参数全程锁死不动。右边是把它们去色后的样子——去色暴露真实的感知亮度。
色相扫描:HSL 与 OKLCH 对照
灰度镜像 = 感知亮度S 锁 100%
C 锁 0.12
正在计算…
lighten() 之类的函数在不同色相上结果完全不可控——HSL 的 L 是「红绿蓝三个数的中点」,一个纯数学量,和眼睛无关。OKLCH 那条灰度带是一整片平的。
这个差异不是审美问题,是工程问题。设计系统里但凡有「hover 时变亮 5%」「danger 色和 primary 色要看起来一样重」这类规则,在 HSL 下就必须为每个色相手工微调,而且改一个色就要重调一遍。
HSL 还有第二个毛病:调饱和度会顺带改色相。Evil Martians 举过一个例子,把 lch(0.35 C 300) 的饱和度从 110 降到 40,蓝色一路漂成了紫色。而 OKLCH 的 C 轴是正交的——降饱和只是降饱和。
02坐标系
L、C、H 各管一件事
OKLCH 是 Oklab 的柱坐标写法:同一根 L 轴,把 Oklab 的 a/b 直角坐标换成了「离轴多远(C)+ 朝哪个方向(H)」。三个轴分别对应人描述颜色时会说的三件事。
拆解器
拖动任一轴,观察另外两个维度是否被牵动| 轴 | 范围 | 含义 | 拿它做什么 |
|---|---|---|---|
| L | 0 – 1 或 0% – 100% |
感知亮度。0 是黑,1 是白 | 整套明暗阶梯、深浅模式互换、对比度的第一近似 |
| C | 0 – 0.4 百分比时 100% = 0.4 |
离灰轴的距离。0 就是纯灰 | 主色/柔和色/禁用态的浓淡差别 |
| H | 0 – 360 可写 deg / rad |
色相角。0° 与 360° 同色 | 配色关系:互补 +180°,三分 ±120° |
色相角不通用
OKLCH 的 0° 不是红色,是洋红;红大约在 29°,黄 90°,绿 142°,蓝 264°,紫 328°。把 HSL 的角度直接搬过来一定错位。
C 的上限为什么写不死
规范说 C 理论上无上限,MDN 给的实用区间是 0 到 0.4。真实上限取决于「这个 L 和这个 H 组合下,屏幕还能不能显示出这么浓的颜色」——每个色相的天花板都不一样,下一节就在讲这件事。
我用二分法把 sRGB 立方体扫了一遍:sRGB 内能达到的最大 C 是 0.321(洋红方向,L≈0.70),Display P3 内是 0.363(绿色方向,L≈0.85)。所以你在 CSS 里写 oklch(0.7 0.35 264) 这种值,浏览器只会把它映射回能显示的最近颜色。
03色域
天花板是波浪形的
色域(gamut)是「能显示哪些颜色」的范围,色彩空间(color space)是「怎么给这些颜色编号」的坐标系。OKLCH 是坐标系,sRGB 和 P3 是范围——坐标系能写出范围之外的编号。
各色相的色度天花板
横轴色相 0–360°两条曲线的形状说明了一切:天花板随色相剧烈起伏。在 L=0.70 这一层,绿色方向(142°)能到 C=0.236,而青色方向(180°)只能到 0.127——差了将近一倍。这就是为什么「所有主色都用 C=0.2」这种规则会翻车:一部分色相根本达不到。
P3 那条线整体更高,红、绿、洋红方向的增益最明显。这部分多出来的颜色,在 Mac、iPhone、新款 iPad 上是真能看到的,在老式 sRGB 屏上会被压回边界。
| 色域 | 覆盖 | 典型设备 | CSS 写法 |
|---|---|---|---|
| sRGB | 基准 | 大多数外接显示器、Windows 笔记本 | #hex rgb() hsl() |
| Display P3 | 约 +25~30% | Apple 全系、多数旗舰安卓、OLED | color(display-p3 …) |
| Rec.2020 | 更大 | HDR 电视、专业监视器 | color(rec2020 …) |
oklch() 本身不绑定任何色域——它只是坐标。写下的值落在哪个范围内,由数值决定;超出显示设备能力时,浏览器做色域映射(gamut mapping),把它压到边界上最接近的颜色。
要不要为 P3 单独写一套
多数情况下不必:直接写 OKLCH,让宽色域屏自然吃到更浓的颜色,窄色域屏自动降级。只有当你要在 P3 屏上刻意做得更艳、同时保证 sRGB 屏不失真时,才分开写:
/* 兜底在前,支持者用后面这条 */ color: #c8342a; color: oklch(0.55 0.19 27); /* 只在宽色域屏上加浓 */ @media (color-gamut: p3) { .brand { color: oklch(0.55 0.24 27); } }
04插值
渐变里的灰死区
两个颜色之间怎么过渡,取决于在哪个坐标系里画直线。同样的两端,坐标系不同,中间段可以差出一个「泥浆色」。
同两端,四种插值路径
留意中段CSS 里可以直接指定插值空间,语法是在渐变方向后面加 in <space>:
/* 默认:sRGB 插值,中段可能发灰 */ background: linear-gradient(90deg, blue, yellow); /* 感知均匀插值 */ background: linear-gradient(in oklch 90deg, blue, yellow); /* 指定色相走短弧还是长弧 */ background: linear-gradient(in oklch longer hue 90deg, blue, yellow);
同一套规则也适用于 color-mix():
color: color-mix(in oklch, var(--brand) 70%, white);
用 oklab 还是 oklch 插值
in oklab 走直角坐标的直线,中段会经过更低的饱和度,过渡最平顺;in oklch 走极坐标,绕着色环转,饱和度全程保持。做柔和过渡用前者,做彩虹/色环用后者。
05语法
写进 CSS
函数形式 oklch(L C H / A),分量之间用空格,透明度用斜杠分隔。
/* 基本形 */ color: oklch(0.7 0.12 250); color: oklch(70% 0.12 250); /* L 可用百分比 */ color: oklch(70% 30% 250deg); /* C 用百分比时 100% = 0.4 */ color: oklch(0.7 0.12 250 / 0.5); /* 半透明 */ color: oklch(0.7 0.12 250 / 50%); /* none:该通道无值,参与插值时行为不同于 0 */ color: oklch(0.7 0 none);
相对色语法:真正省事的地方
from 把一个已有颜色拆成 l/c/h/alpha 四个变量,然后你只改想改的那个。这是 OKLCH 在设计系统里最大的实用价值——一个基色派生出整套状态色。
:root { --brand: #3b82f6; }
/* 只提亮,色相与浓度原样保留 */
.btn:hover { background: oklch(from var(--brand) calc(l + 0.08) c h); }
.btn:active { background: oklch(from var(--brand) calc(l - 0.06) c h); }
/* 只抽掉浓度,做禁用态 */
.btn[disabled] { background: oklch(from var(--brand) l calc(c * 0.25) h); }
/* 只转色相,做互补强调色 */
.accent { background: oklch(from var(--brand) l c calc(h + 180)); }
/* 只改透明度,做焦点环 */
.btn:focus-visible { outline-color: oklch(from var(--brand) l c h / 0.4); }
calc 里不能混百分比
from 拆出的 l 是 0–1 的数值,不是百分比。写 calc(l - 10%) 无效,必须写 calc(l - 0.1)。
兼容性与兜底
Chrome 111、Edge 111、Safari 15.4、Firefox 113 起支持,2023 年 5 月进入 Baseline。兜底最省事的办法就是利用层叠——把旧写法放前面,不认识 oklch() 的浏览器会跳过后一行:
.card {
background: #61a3e6; /* 旧浏览器停在这 */
background: oklch(0.7 0.12 250); /* 新浏览器覆盖 */
}
需要按能力分支时用 @supports;相对色语法的支持面比 oklch() 本身窄一些,值得单独探测:
@supports (color: oklch(from red l c h)) { /* 这里才用 from */ }
06实战
用一个公式生成整套色阶
OKLCH 最直接的产出:固定色相、固定一条 L 阶梯,色阶就自己长出来了,而且换任何色相都保持同样的明暗节奏。Tailwind v4 的默认调色板就是这么重做的。
色阶生成器
L 阶梯固定,只换色相与浓度每格上方是级数,下方是该色与白字的 WCAG 对比度;达到 4.5 的加下划线。浓度按中段最浓、两端收窄的钟形分布——两端本来就接近黑白,硬堆浓度只会溢出色域。
落到代码里,一套色阶就是一行 hue 变量加一张 L/C 表:
:root {
--h: 250;
--c-50: 0.03; --l-50: 0.97;
--c-500: 0.15; --l-500: 0.62;
--c-900: 0.07; --l-900: 0.26;
}
.a { background: oklch(var(--l-50) var(--c-50) var(--h)); }
.b { background: oklch(var(--l-500) var(--c-500) var(--h)); }
.c { background: oklch(var(--l-900) var(--c-900) var(--h)); }
换主题色只要改 --h 一个数,整套色阶的明暗关系原样保留。这是 HSL 做不到的——HSL 换色相,明暗关系会整体走样。
深色模式的一个便利
因为 L 是感知量,深浅模式的映射可以写成一条规则:把 L 沿 0.5 翻转、并适度压低 C(暗背景上高浓度色更刺眼)。比逐个手挑深色版省事得多。
07别踩
四个常见误解
OKLCH 解决了很多问题,但它不是万能的,有几处流传的说法需要修正。
一、L 相同 ≠ 对比度相同
这是最值得记住的一条。WCAG 对比度算的是 sRGB 相对亮度(一个加权的物理光量),和 Oklab 的感知亮度 L 是两把不同的尺子。同一个 L,不同色相,对比度实测能差 13%:
| 颜色(均为 L=0.62, C=0.14) | 色相 | 对白字的对比度 |
|---|---|---|
| 红方向 | 20° | 3.90 |
| 黄方向 | 90° | 3.66 |
| 蓝方向 | 264° | 3.70 |
| 绿方向 | 142° | 3.45 |
所以 OKLCH 让对比度更可预测(同 L 的结果聚在一个窄带里,不像 HSL 那样发散),但不能替代对比度计算。卡在 4.5 边缘的组合仍然必须实测。
二、L=0.5 不是中灰
oklch(0.5 0 0) 渲染出来是 #636363,而不是 #808080。反过来,sRGB 的中点灰 #808080 在 Oklab 里的 L 是 0.60。原因是 L 追的是人眼感知——人眼对暗部更敏感,所以「感知一半亮」的物理亮度远低于一半。从 HSL 迁移时按数字直接搬会整体偏暗。
三、色域映射各家实现不一致
当你写的颜色超出屏幕色域,规范推荐了一套映射算法,但 Chrome 和 Safari 目前用的是更快但更粗糙的做法。后果是超出色域的高浓度颜色在不同浏览器上可能不完全一致。想要跨浏览器像素级一致,就把颜色控制在 sRGB 范围内——上面第 03 节的天花板图可以用来检查。
四、Oklab 本身也不完美
Raph Levien 做过一次系统评测,结论是 Oklab 在色相线性和亮度预测上都显著优于 CIELAB(CIELAB 在蓝色区有严重色相偏移),但也指出传递函数在暗部的对比分辨还有改进空间。它是目前工程上最好用的折中,不是终极答案。
迁移建议
不要用转换工具把现有 hex 一键换成 OKLCH——那只是换了个写法,不会带来任何好处。真正的收益来自重新用 L/C/H 的语言定义色阶:定一条 L 阶梯、定色相、让色值自己算出来。
08接下来
工具与原始资料
动手工具
- oklch.com — Evil Martians 的取色器,能直接看到色域边界在哪,是最常用的一个
- huetone.ardov.me — 基于 Oklab 生成整套无障碍色阶,带对比度矩阵
- npx convert-to-oklch — 批量转换现有代码库的 CLI
- stylelint-gamut — lint 阶段拦截超出色域的值
- OkColor — Figma 插件,把 OKLCH 带进设计稿(Figma 尚无原生支持)
- Color.js / culori — JS 端做转换与色域检测的库
值得读的原文
- Ottosson《A perceptual color space for image processing》 — Oklab 的原始提案,含推导与转换矩阵
- Evil Martians《OKLCH in CSS: why we moved from RGB and HSL》 — 最好的工程视角入门
- MDN
oklch()— 语法权威,相对色语法示例最全 - Smashing《Falling For Oklch》 — 色域与渐进增强讲得最细
- Levien《An interactive review of Oklab》 — 批判视角,了解局限性
- Smashing 对 Ottosson 的访谈 — 设计动机与取舍
本页所有色块由浏览器原生 oklch() 渲染;色域边界、对比度与 hex 数值由页内 JS 用 Ottosson 转换矩阵实算,转换结果已对红/绿/蓝/白四个基准点校验通过。