我有一支技术全面、经验丰富的小型团队,专注高效交付中等规模外包项目,有需要外包项目的可以联系我
最近有个场景很典型。
一个组件库在排查主题 BUG 的时候,突然发现了一件让人当场沉默的事:
这个库里居然有 180 个颜色变量。
品牌主色改一下,要在 3 份文件里同步 15 种深浅、hover、透明度……
在做设计系统的人,大多都听说过“设计 token”:
颜色、间距、字号,全都变量化
常见做法就是:一个 token 对应一个固定的十六进制或 RGB 值
问题是——只要产品一说“全站换个主题色”, 这套“死值系统”立刻变成噩梦现场。
下面的这个 CSS 特性,正好卡在这个痛点上:
可以基于一个“基色”,动态算出各种深浅、明暗、透明度变化。
以后只要改一个变量,整套 UI 跟着自动联动。 颜色体系终于不再靠人肉维护。
传统做法:靠人肉复制的“颜色农场”
绝大多数设计系统的配色,大概长这样:
先定一个基础色
然后给按钮、边框、Hover、Active、背景再各配一堆“手工调色”版本
:root {
--color-primary: #3b82f6;
--color-primary-hover: #2563eb;
--color-primary-active: #1d4ed8;
--color-primary-light: #93c5fd;
--color-primary-dark: #1e40af;
--color-secondary: #8b5cf6;
--color-secondary-hover: #7c3aed;
--color-secondary-active: #6d28d9;
/* ... 后面还会继续长下去 */
}
一个主色系,往往就要十几二十个变量。 调色板一扩,全局颜色 token 数量轻松破百。
设计同学轻飘飘地一句:
“主色想从偏蓝一点,调成更紫一点。”
工程侧就得:
改 15 个变量
对着设计稿比 hover、active 是否协调
还要小心半透明背景有没有漏改
漏一个,hover 怪异; 漏两个,整套主题就开始“脏”。
这种模式:
十六进制来回复制
手动算 RGBA
一遍遍打开取色器微调
最终只有一个评价:费神又不可靠。
CSS 相对颜色:把所有“深浅变化”交给浏览器算
所谓“相对颜色(relative colors)”,指的就是:
新颜色不是写死,而是“从一个基准色算出来”。
先声明一个基色,再用 CSS 按规则派生出各种变化版。 改一次基色,全家跟着变。
语法核心是一个 from 关键字,用起来像这样:
color-function(from origin-color channel1 channel2 channel3 / alpha)
拆开看就很清晰:
color-function:输出格式,比如
rgb()、hsl()、oklch()from:关键字,表示“下面这颜色是来源”
origin-color:基准颜色,hex / rgb / hsl 都行
channel1 ~ 3:可访问和修改的通道值
alpha:可选透明度,写在斜杠后面
最常用的场景,大概是这样:
:root {
--primary: #3b82f6;
}
.button {
background: var(--primary);
}
.button:hover {
background: hsl(from var(--primary) h s calc(l - 10));
}
只多写了一行 hsl(from ...), 却把 hover 效果彻底从“写死”变成“相对基色、自动联动”。
以后品牌色只要改一个 --primary, 所有 hover、active、浅色版,统统自己跟上。
from:让颜色“拆开来用”的魔法词
from 做的事情只有一件:
把一个颜色,转成当前色彩空间下的各个通道, 然后把这些通道值“暴露”出来给后面用。
比如:
rgb(from green r g b) /* 绿会被转成 r=0 g=128 b=0 */
得到通道值之后,就可以为所欲为:
rgb(from green g g g) /* rgb(128 128 128) - 拿绿色通道当灰度用 */
rgb(from green b r g) /* rgb(0 0 128) - 通道顺序随便换 */
刚看到会觉得有点怪, 但一旦接受这种“通道是乐高”的设定,就会发现好玩得离谱。
自带色彩空间转换:源格式随意,结果统一
from 后面跟的颜色格式随意:RGB、HSL、hex 都行。 浏览器会先自动转换到指定的色彩空间,再拆成通道。
hsl(from rgb(255 0 0) h s l) /* 把红色从 RGB 转成 HSL */
oklch(from #3b82f6 l c h) /* 把 hex 蓝转成 OKLCH */
好处是:
设计 token 可以是 hex
业务侧使用可以统一为 HSL 或 OKLCH
中间的转换全部交给浏览器,无需心算
对设计系统而言,这一点尤其舒服:源头怎么存不重要,使用端永远用的是同一套可计算空间。
calc():对色彩通道做“加减乘除”的那一层
真正能落地的地方,是 calc():
通道值可以被
calc()接管通道本身可以当变量来算
比如:
/* 变亮:提高 lightness */
hsl(frombluehscalc(l + 20))
/* 变暗:降低 lightness */
hsl(frombluehscalc(l- 20))
/* 半透明:动 alpha 通道 */
rgb(frombluergb / calc(alpha * 0.5))
/* 调色:旋转 hue */
hsl(frombluecalc(h + 180) sl)
大部分常见的“颜色衍生逻辑”, 其实都是:
通道 + 某个量
通道 * 某个系数
把这些操作写进 CSS, 颜色体系就从“靠感觉”变成“有公式”。
OKLCH:比 HSL 更接近人眼的“真实亮度”
HSL 这套模式,很多前端都用得很顺手:
H — 色相
S — 饱和度
L — 亮度
问题是:它的“亮度”不是真的人眼感知亮度。
比如下面两种颜色,HSL 里都是 50% 光度:
hsl(220 80% 50%) /* 蓝 */
hsl(120 80% 50%) /* 绿 */
理论上亮度一样, 但人眼看过去,绿色明显更亮。
OKLCH 就是为解决这类问题而生的“感知均匀色彩空间”:
同样的 L 值,对不同色相来说,肉眼看上去亮度更一致
做“程序化调色”时,效果更可控
比如:
oklch(55% 0.15 260) /* 蓝 */
oklch(55% 0.15 140) /* 绿 */
两个颜色在人眼中的亮度更接近。
OKLCH 的三要素
结构看起来有点像 HSL,但语义不同:
L(Lightness):0–1 或 0%–100%,0 是黑,1 是白
C(Chroma):0–约 0.37,指颜色的“纯度/强度”
H(Hue):0–360,色相角度
举例:
oklch(0.6 0.2 265) /* 中等亮度、中等纯度、偏蓝 */
可以把 chroma 理解成“离灰色有多远”:
0是纯灰数值越高越艳丽
为什么 OKLCH 对设计 token 尤其重要?
原因就一句话:
在 OKLCH 里,通道变化更可预期。
比如 L 增加 0.1:
不管是蓝、绿、黄,整体“看起来变亮”的感觉接近
不会出现某些色系“过曝”,某些色系“没变多少”的情况
对于一套用算法生成的配色体系来说,这点太关键了:自动算出来的深浅层级,不再依赖运气。
用相对颜色 + OKLCH,搭一套真正“聪明”的设计 token 系统
下面是一套可落地的 token 模式。
第一步:只定义“品牌基色”
:root {
/* 基础品牌色,用 OKLCH 存 */
--brand-primary: oklch(0.550.2265);
--brand-success: oklch(0.650.18145);
--brand-error: oklch(0.60.2525);
--brand-warning: oklch(0.750.1585);
}
四个颜色,就够当整套系统的“根”。 别的全部相对它们推导。
第二步:按规则生成完整色板
:root {
/* Primary 体系 */
--primary: var(--brand-primary);
--primary-hover: oklch(from var(--brand-primary) calc(l - 0.1) c h);
--primary-active: oklch(from var(--brand-primary) calc(l - 0.15) c h);
--primary-light: oklch(from var(--brand-primary) calc(l + 0.2) calc(c * 0.5) h);
--primary-lighter:oklch(from var(--brand-primary) calc(l + 0.3) calc(c * 0.3) h);
--primary-alpha-10:oklch(from var(--brand-primary) l c h / 0.1);
--primary-alpha-20:oklch(from var(--brand-primary) l c h / 0.2);
/* Success 体系 */
--success: var(--brand-success);
--success-hover: oklch(from var(--brand-success) calc(l - 0.1) c h);
--success-light: oklch(from var(--brand-success) calc(l + 0.2) calc(c * 0.5) h);
--success-alpha-10: oklch(from var(--brand-success) l c h / 0.1);
/* Error 体系 */
--error: var(--brand-error);
--error-hover: oklch(from var(--brand-error) calc(l - 0.1) c h);
--error-light: oklch(from var(--brand-error) calc(l + 0.2) calc(c * 0.5) h);
--error-alpha-10: oklch(from var(--brand-error) l c h / 0.1);
}
四个基色,扩展出一整板:
默认色
Hover / Active
浅色版 / 更浅的提示色
不同透明度的覆盖层
以后品牌主色要改?只动四个基变量。
暗色模式:不再需要复制一套“深色 token”
暗色模式的写法,这一段很出圈:
:root {
--surface: oklch(0.980.02240);
--text: oklch(0.250.03240);
}
[data-theme="dark"] {
/* 通过“反转亮度”做暗色 */
--surface: oklch(from oklch(0.980.02240) calc(1 - l) c h);
--text: oklch(from oklch(0.250.03240) calc(1 - l) c h);
}
浅色主题里:
背景亮、文字暗
到暗色主题里,两者的 L 值反转
结果是:
文字和背景的对比关系整体保持一致
只通过公式完成“明 / 暗模式”的整体迁移
不再需要写两套完全独立的 token
实战里的几个高级模式
这些模式在真实项目里解决过不少麻烦。
1)半透明遮罩:用同一个基色延伸
模态框背景的遮罩层:
.modal-backdrop {
background: oklch(from black l c h / 0.7);
}
也可以把 black 换成主题色, 快速生成带品牌色调的半透明遮罩。
2)动态阴影:卡片背景变了,阴影自动跟随
.card {
--card-bg: var(--primary);
background: var(--card-bg);
box-shadow:
04px6pxoklch(from var(--card-bg) l c h / 0.2),
010px15pxoklch(from var(--card-bg) l c h / 0.15);
}
阴影颜色不再写死,而是:
自动从卡片背景里“偷”一份颜色
换肤 / 换主题时,阴影会自然跟着变
3)可读性友好的对比色:自动拉开 60% 光度差
.tag {
--tag-bg: var(--primary);
background: var(--tag-bg);
/* 文本比背景亮 0.6,保证对比度 */
color: oklch(from var(--tag-bg) calc(l + 0.6) c h);
}
.tag--dark {
--tag-bg: oklch(0.30.15200);
/* 若背景偏亮,就反向压暗文本 */
color: oklch(from var(--tag-bg) calc(l - 0.6) c h);
}
L 通道的差值大约在 0.6 左右, 通常能得到相对可靠的可读对比度。
浏览器支持与回退策略
截至 2025 年 10 月,相对颜色的支持情况大致是:
Chrome 119+
Firefox 128+
Safari 16.4+
Edge 119+
覆盖率大约在 83% 左右。
老旧环境可以:
先写一层“静态颜色”的 fallback
再用相对颜色覆盖一层
保证老环境能“看到”,新环境能“用好”。
初次上手最容易踩的坑(
几个容易犯错的点,提前列出来:
链式派生太多层
一层颜色从另一层推导,再从推导色继续推导……
层级越深,后期越难排查
建议“从基色直接推导”,最多两层
把 chroma 拉超过设备能表示的范围
OKLCH 在 sRGB 上 chroma 一般到 ~0.37
再往上加,颜色可能会“怪异溢出”
写错 alpha 位置
正确写法是:
oklch(0.6 0.2 265 / 0.5)而不是:
oklch(0.6 0.2 265 0.5)
提前知道这些坑,能少掉很多“为什么和设计稿不一样”的困惑。
最后这点小升级,解决的是大系统的病
从设计系统的角度看,这个特性实际上解决了几件“大事”:
主题切换不再是大手术
颜色 token 可以真正“集中管理”
深浅、Hover、Active、透明度,统统变成可复用的“规则”而不是手工调的“结果”
下一次配色系统要重构的时候,不妨试着把:
基准品牌色
暗色模式
状态色
半透明覆盖层
全部交给相对颜色来算一遍。 那种“改一个变量,全站自洽”的感觉,非常上头。
来自发起人的一小段话
这份内容背后,是一个长期在前端与设计系统领域打磨的小团队。 Sunil 在结尾留过一句话,大意是:
这些内容背后,没有外部资金, 只是想为每月 350 万读者的技术社区多加一点实用的东西。
如果这些“微小的 CSS 小技巧”有帮上忙, 最好的反馈方式,就是把知识在自己的项目里用起来,再分享给下一位还在为设计系统抓狂的同事。
全栈AI·探索:涵盖动效、React Hooks、Vue 技巧、LLM 应用、Python 脚本等专栏,案例驱动实战学习,点击二维码了解更多详情。

最后:
CSS相对颜色拯救设计系统

被折叠的 条评论
为什么被折叠?



