为什么 CSS 颜色需要一次升级
做前端做到一定年头,很多人都有这么一种体会:CSS 里处理颜色的方式,十几年好像没怎么变过。hex、rgb()、hsl(),最多配合 CSS 变量和预处理器的颜色函数,就已经是大多数项目的全部手段了。

这套老办法不是不能用,而是在一些真实工程场景里越来越别扭。比如设计系统里定义了一套品牌色,hover 状态需要压暗一点,active 状态需要更暗一点。过去常见的做法是在 Sass 里调 darken() 或 mix(),编译时生成几个色值,然后硬编码到样式里。问题在于,这套色值一旦生成,就和运行时的主题系统没什么关系了。如果用户切换暗色模式,或者业务上需要换肤,老办法要么重新编译,要么写一堆重复的变量。
另一个问题来自设备本身。现在越来越多的手机和笔记本屏幕能够显示广色域,设计师在 display-p3 下调整出来的颜色,到了 CSS 里一旦被转成 sRGB,常常会让人觉得“颜色灰了一层”。换句话说,颜色这件事的复杂度已经超过了旧工具能处理的范围。
CSS Color Level 5 正是冲着这些问题来的。它带来的不是一个单独的函数,而是三个互相支撑的能力:color-mix() 把颜色混合从预处理器搬到了浏览器,相对颜色语法让开发者在已有颜色基础上做通道级调整,而 oklch / oklab 这些新的色彩空间则让这些操作在感知上更可靠。
color-mix():把颜色混合交给浏览器
color-mix() 的用法看起来很直接,就是在一个指定的色彩空间里混合两种颜色。最基本的写法是这样:
color-mix(in oklch, var(--brand) 80%, black);
color-mix(in srgb, #ff9900 40%, #00cc99);
第一个例子把品牌色和黑色按 80% 和 20% 混合,得到一个更深的品牌色。第二个例子在 sRGB 空间里混合橙和青。这里的 in 关键字决定的是插值所用的色彩空间,它不只是一个执行细节,而是会直接影响最终看到的颜色。
在 sRGB 里混合红和蓝,得到的是偏灰的紫色;在 oklab 里混合同样的两个颜色,会因为亮度感知更均匀而保留更干净的饱和感。对设计系统来说,这个差别不是玄学,是实实在在的观感差异。
百分比的使用有一点规则。两个颜色各自携带的百分比代表权重,如果两边都省略,默认就是各 50%。如果合计超过 100%,函数会等比缩放到 100%;如果合计不足 100%,则剩余部分会以透明的形式体现在结果里。实际开发中最常用的写法,仍然是让两个比例相加等于 100%。
真正需要留意的是,color-mix() 并不等同于预处理器里的混色。Sass 的 mix() 本质上是在 sRGB 空间的算术平均,而且它是编译期的产物,最终输出的是一串静态色值。而 color-mix() 是运行时的 CSS 函数,它可以接受 CSS 变量,可以参与主题切换,也能和 transparent 这样的颜色做自由混合。这带来的直接变化是:设计系统不需要为每个按钮状态预先编译出一套色阶,而是用一个变量加一次函数调用动态表达出来。
.btn {
background: var(--brand);
}
.btn:hover {
background: color-mix(in oklch, var(--brand) 85%, black);
}
.btn[data-variant='secondary'] {
--btn-base: var(--gray-6);
}
这段代码没有在任何地方写死“暗色 5%”的具体色值,而是让浏览器在渲染时根据当前变量实时计算。如果项目支持多个主题,每个主题只需要调整 --brand,hover 状态也会跟着变。
相对颜色语法:从已有颜色出发做精细调整
color-mix() 适合做混合,但有些需求更像是“把某个颜色的透明度调低”,或者“在保持色相不变的前提下把明度提高一点”。过去我们通常先转换成 HSL 再手工计算,或者依赖 JS 把颜色拆开。CSS Color Level 5 提供了相对颜色语法,直接给出一个颜色源,然后把它的通道映射到新颜色的对应通道上。
/* 保持相同的色相、饱和度和亮度,只改透明度 */
background: oklch(from var(--brand) l c h / 0.5);
/* 把明度降低 0.1 */
background: oklch(from var(--brand) calc(l - 0.1) c h);
/* 改成用户主题色相 */
background: hsl(from var(--brand) var(--theme-hue) s l);
from 后面的颜色被称为“源色”,然后在新颜色函数中可以直接引用 r、g、b、h、s、l、c、alpha 等通道关键字。它们不是字符串,而是可以参与计算的值。这意味着写样式时,可以像在 JS 里操作颜色那样做算术运算。
一个很常见的场景是阴影和描边。比如需要为按钮生成一个带透明度的边框:
border: 1px solid hsl(from var(--brand) h s l / 0.4);
这块能力对主题系统尤其有用。过去如果要支持品牌色可切换,透明度的版本往往需要额外生成变量,比如 --brand-40。现在可以直接用相对颜色语法在用的时候计算,变量数量会少很多。
这里有一个很容易踩的坑:相对颜色语法中的 from var(--color),如果 --color 本身又是这个函数的一部分,很容易形成循环依赖。
:root {
--brand: oklch(from var(--brand) calc(l + 0.1) c h);
}
这段代码看起来是想让品牌色变亮一点,实际上会让变量陷入循环定义,结果就是整个属性失效。相对颜色更适合“从静态颜色或主题变量派生新颜色”,不要在源色里引用自己。
为什么现代色彩空间如此重要
很多人一开始不理解,既然 color-mix() 也能指定 sRGB,为什么还要折腾 oklch。这里的关键在于颜色的感知均匀性。
sRGB 和 HSL 都是基于旧显示器的色彩模型。sRGB 的 RGB 通道对亮度的感知不是线性的,HSL 的 H 和 L 更是一对很难直觉调整的感受量。比如在 HSL 里把明度从 50% 调到 60%,对黄色和蓝色产生的“变亮”效果完全不同。
oklch 这一类感知均匀的色彩空间,它的目标就是让数学上的等量变化,在视觉上也看起来等量。这样做的收益在设计系统里非常明显:从品牌色到 hover、active、disabled 的一套色阶,可以用统一的 delta 计算出来,而不是靠设计师肉眼微调。
不同色彩空间的适用性并不相同。下面这张表可以作为选择时的参考。
| 色彩空间 | 特点 | 常见用途 | 注意事项 |
|---|---|---|---|
| sRGB | 兼容性最好的 RGB 空间 | 普通 UI、与设计稿精确对齐 | 色域窄,直接混合容易偏灰 |
| HSL | 类似人的直觉,容易上手 | 手动调整色相、明度 | 感知不均匀,明暗变化不稳定 |
| LCH | 亮度和色度分离 | 需要高饱和度颜色 | 色相变化有边界怪异问题 |
| OKLCH | 感知均匀,算法更适合现代 UI | 混合、色阶、主题派生 | 部分旧浏览器不支持 |
对于大部分前端项目,我的建议是:日常业务代码继续用 sRGB 或 hex 没问题,但凡是需要做颜色计算、混合、主题派生的地方,优先用 oklch。它带来的并不只是“更现代”的感觉,而是切切实实地减少了颜色看起来“脏”的概率。
渐进增强:把这些能力用到真实项目里
前面讲了不少特性,但落到实际项目里,还是绕不开浏览器兼容。
目前 color-mix() 和相对颜色语法在主流浏览器中都已经有相当不错的支持。不过开发时仍然建议采用渐进增强策略,把旧方案作为回退,新能力作为增强。
.btn {
background: #c43d2e;
}
@supports (background: color-mix(in oklch, red, blue)) {
.btn {
background: color-mix(in oklch, var(--brand) 85%, black);
}
}
借助 @supports,不支持新语法的浏览器会直接使用第一个静态色值,支持的在后面覆盖。这样既不会影响页面可用性,也能让现代浏览器获得更好的颜色表现。
在做设计系统时,可以按这样三个阶段去推进:
- 先把主题核心变量换成 oklch 或能明确定义的颜色,让后续计算有可靠的基础。
- 在有需要做 hover、active、边框色、文本次级色的地方,引入
color-mix()替代预编译的色阶。 - 对于透明度、亮度微调这类需求,优先使用相对颜色语法,减少冗余变量。
实际项目中比较常见的做法是:先用 CSS 变量定义品牌色,然后用相对颜色派生透明度版本,用 color-mix() 来生成 hover 和 active 色。这样整个主题只需要少量核心变量,就能覆盖大多数颜色状态。
另外要提醒一句:不要让 color-mix 出现在每一条样式里。如果某个颜色被大量组件复用,把它定义成一个 CSS 变量,而不是到处重复调用函数。这样可以避免渲染时重复计算,也方便后续替换。
哪些场景值得用,哪些还需要等等
现在的 CSS 颜色体系比几年前复杂了不少,但并不是所有地方都需要立刻迁移。
如果你的项目只是静态页面,色彩也很少变动,那么继续用 hex 或 rgb 完全没问题。新特性不会让你的页面变好,反而可能增加维护成本。
但如果你的项目有这样的特征:有设计系统、有换肤需求、有颜色深色模式适配,或者团队经常为了颜色微调而反复发版,那 CSS Color Level 5 的这些能力就非常值得落地。它们的价值不只是少写几行 Sass,而是把颜色从“编译期产物”变成了“运行时可计算的值”。这中间的区别,直接决定了主题系统能灵活到什么程度。
还有一个提醒是,不要为了用而用。色彩空间的差异、混合空间的差异,都是真实存在的,但它们对最终用户的影响需要结合具体场景来评估。如果你的界面颜色本来就很收敛,升级到 oklch 的感知差别可能并不大;而在大面积使用渐变、强调色的品牌型界面里,感知差别会明显得多。
总体而言,CSS Color Level 5 是近年 CSS 颜色体系里最重要的一次演进。color-mix() 解决了颜色混合的运行时问题,相对颜色语法解决了精细调整的表达问题,oklch 则让前两者在一个更可靠的视觉模型上工作。这三样东西组合在一起,让前端在颜色这件事上终于有了和设计语言匹配的工具。
原创文章,作者:fudengji,如若转载,请注明出处:https://fudengji.cn/article/818/