配了五年色板,每次想从主色派生个深色都要写一堆变量——今天 CSS 把这件事用一行彻底原生化了

设计师只给了你一个品牌色,你要生成一整套色板:深色悬停态、次级按钮色、边框描边色、hover 加深、active 加亮……以前要么开 preprocessor 循环,要么接一个 JS 颜色库,要么硬编码七八行。

今天 CSS 直接在浏览器里把这件事做完了,一行 oklch(from var(--brand) calc(l * 0.8) c h),不用任何构建工具。

这个能力叫 CSS 相对颜色语法(Relative Color Syntax),CSS Color Level 5 的核心新特性。Chrome 119、Firefox 128、Safari 16.4 全部支持,覆盖率 ~92%,已经在生产可用阶段。

语法:从一个颜色拆出通道

相对颜色语法的核心是 from 关键字。它告诉 CSS:「把这个颜色拆开,给我它的通道值」。

/* 把 --brand 拆成 oklch 的 L、C、H 三个通道 */
--brand: oklch(60% 0.2 250);

/* 直接复用,不做修改,结果还是同一个颜色 */
--brand-copy: oklch(from var(--brand) l c h);

/* 把亮度降低 30% */
--brand-dark: oklch(from var(--brand) calc(l * 0.7) c h);

/* 把亮度提高 15% */
--brand-light: oklch(from var(--brand) calc(l + 0.15) c h);

/* 把色度提高 30% */
--brand-saturated: oklch(from var(--brand) l calc(c * 1.3) h);

/* 把色相旋转 60 度 */
--brand-rotated: oklch(from var(--brand) l c calc(h + 60));

from 后面的颜色可以是任何格式——HEX、HSL、RGB、OKLCH 都可以,CSS 会自动把它转换到前面函数指定的色彩空间再拆通道。

为什么选 OKLCH 而不是 HSL

这里有个细节:上面的例子全用了 oklch() 而不是 hsl()。这很重要。

HSL 的 L 值不是感知均匀的——hsl(60 100% 50%)(黄色)和 hsl(240 100% 50%)(蓝色)视觉上亮度完全不一样,但数值都是 50%。用 HSL 做色阶,相邻档之间的视觉跳变不均匀。

OKLCH 的 L 轴是感知均匀的——等步长的 L 值变化,视觉上看起来是等距的。所以用 OKLCH 做色阶:

/* 从一个品牌色,生成完整的 10 档色阶 */
--brand-50:  oklch(from var(--brand) calc(l + 0.35) c h);
--brand-100: oklch(from var(--brand) calc(l + 0.28) c h);
--brand-200: oklch(from var(--brand) calc(l + 0.20) c h);
--brand-300: oklch(from var(--brand) calc(l + 0.12) c h);
--brand-400: oklch(from var(--brand) calc(l + 0.05) c h);
--brand-500: oklch(from var(--brand) l c h);               /* 原色 */
--brand-600: oklch(from var(--brand) calc(l - 0.05) c h);
--brand-700: oklch(from var(--brand) calc(l - 0.12) c h);
--brand-800: oklch(from var(--brand) calc(l - 0.20) c h);
--brand-900: oklch(from var(--brand) calc(l - 0.30) c h);

每一档之间的视觉距离是均匀的。这在以前需要设计师用专业工具算,或者你用 JS 库跑一遍 OKLab 转换,现在一行 CSS 搞定。

配合 color() 访问宽色域

CSS Color Level 5 还扩展了 color() 函数,让它也支持相对颜色语法。这意味着你可以从一个颜色提取通道,然后放到 display-p3 等宽色域空间里:

/* 从 sRGB 品牌色提取通道,输出为 Display P3 宽色域 */
--brand-p3: color(from var(--brand) display-p3 r g b);

/* 同时调透明度 */
--brand-p3-alpha: color(from var(--brand) display-p3 r g b / 0.8);

支持的颜色空间包括:srgbsrgb-lineardisplay-p3a98-rgbprophoto-rgbrec2020xyzxyz-d50xyz-d65。如果你在做设计系统并且要覆盖 P3 屏幕,这个能力很关键。

三个具体场景

场景一:暗色模式色板

:root {
  color-scheme: light dark;
  --bg-primary: light-dark(
    oklch(from var(--brand-500) calc(l + 0.35) c h),
    oklch(from var(--brand-500) calc(l - 0.30) c h)
  );
}

配合 light-dark(),同一个 CSS 变量在亮色和暗色模式下自动取不同的派生值,不再需要两组完全独立的变量。

场景二:hover / active 状态

.button {
  background: var(--brand-500);
}
.button:hover {
  /* 悬停时加深 10% 亮度 */
  background: oklch(from var(--brand-500) calc(l - 0.10) c h);
}
.button:active {
  /* 按下时再加深 10% */
  background: oklch(from var(--brand-500) calc(l - 0.20) c h);
}

所有状态颜色从同一个来源派生,设计改品牌色时只需要改 --brand-500 一行,所有派生色自动更新。

场景三:边框 + 文字 + 背景统一派生

.card {
  border-color: oklch(from var(--brand-500) l calc(c * 0.5) h / 0.3);
  color: oklch(from var(--brand-500) calc(l + 0.40) c h);
  background: oklch(from var(--brand-500) calc(l + 0.38) c h / 0.08);
}

边框、文字、背景全部从主色派生,视觉上一致性有保证,不用靠感觉调值。

浏览器支持与渐进增强

相对颜色语法(from 关键字)的浏览器支持:

浏览器 支持版本
Chrome 119+
Firefox 128+
Safari 16.4+
Edge 119+

覆盖约 92% 的全球流量,已经是 Baseline Widely Available 的状态。生产项目可以用 @supports 做渐进增强:

/* 检测支持后再用 */
@supports (color: oklch(from white l c h)) {
  --brand-600: oklch(from var(--brand-500) calc(l - 0.10) c h);
}

三步下一步

第一步: 打开 DevTools,在 Elements 面板里找一个现有的颜色变量,试着加一行 oklch(from var(--existing) calc(l * 0.8) c h),看浏览器是否正常渲染。

第二步: 把品牌主色迁移到 OKLCH——用一个在线转换工具把 HEX 转成 oklch(),然后用相对颜色语法生成所有派生色,感受色阶均匀性的提升。

第三步: 审计项目里的颜色工具库(比如 color.js、chromatic、polished、tinycolor2),看看有多少功能现在可以用原生 CSS 替代,考虑减少一个依赖。

相对颜色语法把「从已有颜色派生新颜色」这件事,从构建工具链和 JS 运行时里拉到了 CSS 声明层。你不需要为了一个颜色工具函数多打一个 npm 包——CSS 本身现在就能算了。

评论区

0 条评论

登录后可评论。

阿柯·前端架构 82 阅读