这点小小 CSS 升级,悄悄救了无数快要崩溃的设计系统

CSS相对颜色拯救设计系统

我有一支技术全面、经验丰富的小型团队,专注高效交付中等规模外包项目,有需要外包项目的可以联系我

最近有个场景很典型。

一个组件库在排查主题 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

  • 再用相对颜色覆盖一层

保证老环境能“看到”,新环境能“用好”。

初次上手最容易踩的坑(

几个容易犯错的点,提前列出来:

  1. 链式派生太多层

  • 一层颜色从另一层推导,再从推导色继续推导……

  • 层级越深,后期越难排查

  • 建议“从基色直接推导”,最多两层

  • 把 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 脚本等专栏,案例驱动实战学习,点击二维码了解更多详情。

    图片

    最后:

    Vue 设计模式实战指南 

    20个前端开发者必备的响应式布局

    深入React:从基础到最佳实践完整攻略

    python 技巧精讲

    React Hook 深入浅出

    CSS技巧与案例详解

    vue2与vue3技巧合集

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值