oklch()、color-mix() 和相对颜色语法怎样做主题色?
oklch() 用感知亮度 L、色度 C、色相 H 描述颜色,调 L 就能得到观感上均匀变亮或变暗的颜色。color-mix() 按比例在指定色彩空间里混合颜色,常用来做悬停色和半透明色。相对颜色语法 oklch(from var(--brand) calc(l - 0.1) c h) 能从一个品牌色直接算出变体。三者都基于浏览器运行时计算,能跟着自定义属性切换主题;相对颜色语法支持得最晚,要用 @supports 或回退值。
简答
做主题时常见的需求是:给一个品牌色,自动得到它的悬停色、按下色、浅色背景和半透明边框。以前要么在设计稿里一个个取色,要么靠 Sass 在构建时算死。现在 CSS 自己就能算,而且能跟着变量在运行时变。
oklch()用三个数描述颜色:感知亮度 L、色度 C(颜色有多“浓”)、色相 H。它的 L 是人眼感觉到的亮度,所以只改 L 就能得到观感上均匀的深浅梯度,hsl()的亮度不是按人眼感知定义的。有了基础色,有两种派生办法。
color-mix(in oklab, var(--brand) 80%, black)按比例把两种颜色混在一起,适合悬停色,和transparent混还能得到半透明色。相对颜色语法oklch(from var(--brand) calc(l - 0.1) c h)先把品牌色拆成 l、c、h 三个通道,再改其中一个,适合精确地调亮度或透明度。要注意兼容性。按 MDN 兼容性数据,
oklch()和color-mix()从 Chrome 111、Firefox 113、Safari 15.4 和 16.2 起支持,相对颜色语法要到 Chrome 122、Firefox 128、Safari 18 才完整支持,需要照顾旧浏览器时要先写一个固定颜色作为回退。
为什么用 oklch 而不是 hsl
oklch() 的写法是 oklch(L C H / A)。L 是 0 到 1(或 0% 到 100%),0 是黑、1 是白;C 是色度,最小为 0(灰色),理论上没有上限,实际不会超过 0.5;H 是色相角度;斜杠后面是可选的透明度。
MDN 特别说明,oklch() 的 L 是人眼感知到的亮度,而 hsl() 的 L 只是相对其他颜色而言的亮度,两者不是一回事。MDN 介绍 color-mix() 时也说 Oklab 是按感知均匀设计的。所以做一组“品牌色 100 到 900”的色阶,固定 C 和 H、只改 L,比在 hsl 里调亮度更接近人眼感觉到的均匀梯度。
还有一个容易记错的地方:oklch() 的色相角度和 hsl() 不同。MDN 举的例子是,hsl() 里 0 度是红色,而在 CIELab 色彩空间里 0 度对应品红,红色大约在 41 度。从 hsl 迁移过来时不能直接照搬色相值。
用 color-mix 派生悬停色和半透明色
color-mix() 接收一个插值方法和若干颜色,返回混合结果:
:root {
--brand: oklch(0.6 0.15 250);
}
.button {
background: var(--brand);
}
.button:hover {
background: color-mix(in oklab, var(--brand) 85%, black);
}
.button:focus-visible {
outline: 3px solid color-mix(in oklab, var(--brand) 40%, transparent);
}
in oklab 是混合所用的色彩空间。MDN 的建议是:要感知上均匀的过渡用 Oklab,想避免混合时颜色发灰用 Oklch,只有需要和某个按 sRGB 工作的设备或软件对齐时才用 sRGB,因为 sRGB 混出来容易偏暗、偏灰。
百分比的规则也要清楚。两个百分比只写一个时,另一个是 100% 减去它;都不写时各占 50%;加起来不等于 100% 时会按比例归一化。如果加起来小于 100%,结果还会乘上这个总和作为透明度,效果相当于和 transparent 混合。上面 outline 的写法就是“品牌色只占 40%,其余是透明”,得到一个半透明的焦点环。
用相对颜色语法精确改一个通道
相对颜色语法在颜色函数里加上 from 和一个原色。浏览器先把原色转换成这个函数对应的色彩空间,再把各通道的值作为 l、c、h、alpha 这几个关键字提供给你:
:root {
--brand: oklch(0.6 0.15 250);
--brand-strong: oklch(from var(--brand) calc(l - 0.1) c h);
--brand-soft: oklch(from var(--brand) 0.95 calc(c * 0.3) h);
--brand-ghost: oklch(from var(--brand) l c h / 0.2);
}
--brand-strong 只把亮度降了 0.1,色度和色相原样保留。--brand-soft 把亮度定为 0.95、色度降到三成,得到同色系的浅背景。--brand-ghost 只改透明度。原色可以是任何合法颜色,包括自定义属性、currentColor,甚至另一个相对颜色,色彩空间也不需要和输出函数一致。
和 color-mix() 相比,相对颜色语法能指定“只改亮度”“只改透明度”这类具体操作;color-mix() 只能指定混合比例,结果怎样取决于和什么颜色混。两者经常一起用。
为什么主题切换时这些派生色会跟着变
派生色写的是 var(--brand),不是某个固定值,所以它们在浏览器里按元素实际拿到的 --brand 计算。深色主题里只要重新定义 --brand 这一个变量,悬停色、浅背景、焦点环都会跟着重新算出来,不需要为深色主题再写一套派生色。主题变量的组织方式和切换流程见《怎样实现网站的暗黑模式和主题切换?》,变量怎样分层命名见《设计 token 为什么比散落的颜色变量更适合大型前端项目?》。
Sass 的 darken() 是在构建时算出一个固定值,运行时改不了,这也是原生颜色函数的优势,见《Sass、Less 这类 CSS 预处理器解决了什么问题?原生 CSS 现在能替代它们吗?》。
兼容性怎样处理
下面的版本号来自 MDN browser-compat-data 8.1.7(2026 年 10 月)。
| 特性 | Chrome | Firefox | Safari |
|---|---|---|---|
oklch() | 111 | 113 | 15.4 |
color-mix() | 111 | 113 | 16.2 |
oklch() 相对颜色语法 | 122(119 起部分支持) | 128 | 18(16.4 起部分支持) |
color-mix() 混合多于两种颜色 | 不支持 | 150 | 27 |
所以 color-mix() 目前稳妥的用法是只混两种颜色。需要照顾旧浏览器时,利用 CSS 遇到不认识的值会丢弃整条声明的规则,先写一条固定颜色,再写新语法:
.button:hover {
background: #2f5fb3;
background: oklch(from var(--brand) calc(l - 0.1) c h);
}
不过这个写法对通过 var() 引用的值不起作用。MDN 解释说,浏览器解析自定义属性时还不知道它会用在哪里,几乎所有值都算合法;等替换进 color 这类属性后发现无效,用的是继承值或初始值,而不是退回上一条声明。这种情况可以用 @supports 检测,MDN 给的写法是 @supports (color: hsl(from white h s l)),换成 oklch 同理,只在支持时才定义派生变量。
常见追问
用户自选主题色时,JavaScript 要算出所有派生色吗?
不用。派生色都写成基于
var(--brand)的表达式,JavaScript 只需要用document.documentElement.style.setProperty('--brand', 'oklch(0.6 0.15 250)')改这一个变量,其余颜色由浏览器重新计算。
为什么混合时要写 in oklab,不写会怎样?
插值方法决定在哪个色彩空间里混合,同样两种颜色在 sRGB 和 Oklab 里混出来不一样。MDN 说省略时默认是
oklab,但显式写出来更清楚,读代码的人不用记默认值。