配了三年 CSS,每次写渐变色都要靠设计软件——今天 color-interpolation 把这件事从根上原生化了

配了三年 CSS,每次写渐变色都要靠设计软件——今天 color-interpolation 把这件事从根上原生化了。

渐变色写多了会发现一个问题:设计稿里渐变很鲜艳,到了浏览器里一跑,中间那段变成了灰色。红色到蓝色中间跑出来一个脏兮兮的紫灰色,不是设计师的错,是插值颜色空间的问题。

CSS 现在可以控制渐变在哪个颜色空间里插值了,Chrome 111+ / Firefox 127+ / Safari 16.2+ 就支持,92% 全球覆盖率。

问题出在哪

传统 RGB 插值是按红绿蓝三个通道的数值取平均,HSL 是按色相/饱和度/明度取平均。两种方式都有一个共同缺陷:它们的几何结构和人的视觉感知不对齐

一个从 #ff0000(红)到 #0000ff(蓝)的渐变,在 sRGB 里计算中点:

background: linear-gradient(to right, red, blue);
/* 中间会跑出紫灰色——因为 RGB 插值经过了色相中性地带 */

在 oklch 里,中点会保持高饱和度的紫色,因为 oklch 的几何结构和人的视觉感知对齐,等距的数值差对应等距的感知差。

background: linear-gradient(to right in oklch, red, blue);
/* 中间是鲜艳的紫色——oklch 感知均匀 */

in oklch:让渐变保持饱和

/* 设计稿里的渐变:品红到青蓝 */
.hero-gradient {
  background: linear-gradient(
    135deg in oklch,
    oklch(60% 0.25 340),  /* 品红 */
    oklch(70% 0.18 220)   /* 青蓝 */
  );
}

这个渐变在 oklch 里插值,色相从 340° 往 220° 转的过程中,饱和度不会掉,整个渐变保持高饱和度。放到 srgb 里同样两个色,饱和度会先掉到 20% 再拉回来,中段明显发灰。

配合 color-mix() 一起用效果更好:

:root {
  --brand: oklch(55% 0.22 252);
  --brand-light: color-mix(in oklch, var(--brand), white 30%);
  --brand-dark: color-mix(in oklch, var(--brand), black 25%);
}

.button {
  background: linear-gradient(to bottom in oklch, var(--brand-light), var(--brand));
  border: 1px solid color-mix(in oklch, var(--brand), black 20%);
}

整套按钮系统里的渐变和边框都保持在 oklch 空间,色调稳定。

longer hue:解决色相绕路问题

色相(hue)是个角度,从 0 到 360°。从红色(0°)渐变到青色(180°)时,色相是往前走还是往后绕?

/* 色相插值默认走 shorter——红色到青色走 180° */
background: linear-gradient(to right in hsl, red, cyan);
/* 中间经过绿色调,视觉上偏暗偏脏 */
/* 强制走 longer——绕 360° 到 540°,色相单调递增 */
background: linear-gradient(to right in hsl longer hue, red, cyan);
/* 色相从 0 往 360 走,经过品红、紫、蓝,饱和度始终饱满 */

longer hue 在渐变色跨度超过 180° 时尤其重要,比如互补色渐变(180° 差)。默认的 shorter 会走反方向,导致路径经过灰色地带;longer 强制绕远路,保持高饱和。

increasingdecreasing 是另外两个 hue 插值方向:increasing 强制色相从低到高走,decreasing 强制从高到低走,适合在动画里做可预测的色相循环。

场景一:品牌色渐变体系

一套设计系统的品牌色通常基于一个主色派生。现在不需要手写多个 HEX 值了:

:root {
  --brand: oklch(58% 0.22 252);
  --brand-50: color-mix(in oklch, var(--brand), white 85%);
  --brand-100: color-mix(in oklch, var(--brand), white 60%);
  --brand-200: color-mix(in oklch, var(--brand), white 35%);
  --brand-500: var(--brand);
  --brand-700: color-mix(in oklch, var(--brand), black 25%);
  --brand-900: color-mix(in oklch, var(--brand), black 55%);
}

/* 渐变用 oklch 插值保持饱和 */
.btn-primary {
  background: linear-gradient(
    135deg in oklch,
    var(--brand-200),
    var(--brand-700)
  );
}

同一套 token,改 –brand 一个值,所有渐变和派生色同步更新,而且全程 oklch 感知均匀,不会出现中间色调脏的问题。

场景二:数据可视化色板

数据看板的分类配色最怕中间段发灰。用 oklch 可以保证分类之间视觉距离均匀:

.chart-series {
  --cat-1: oklch(65% 0.22 252);
  --cat-2: oklch(68% 0.20 145);
  --cat-3: oklch(62% 0.22 38);
  --cat-4: oklch(58% 0.20 320);
  --cat-5: oklch(60% 0.18 180);
}

.bar-1 { background: var(--cat-1); }
.bar-2 { background: linear-gradient(to top in oklch, var(--cat-2), var(--cat-1)); }

同一数据系列内的渐变柱状图也用 oklch,不同系列之间的边界始终清晰可辨。

三个坑

1. 嵌套的渐变颜色空间要一致。 如果外层用了 in oklch,内层 color-mix() 也要 in oklch,混用 srgb 和 oklch 会导致某段看起来不一致。解决方案是统一用 oklch 作为整个颜色系统的插值基准。

2. Safari 对某些极低明度色支持不稳定。 Safari 16.4+ 完整支持 hue 插值关键字,但当 lightness 低于 5% 时,某些 oklch 值的转换结果在不同浏览器间存在差异。生产环境避免用极暗的 oklch 值做渐变端点。

3. 渐变色动画慎用 hue 插值。 如果渐变色要做 CSS transition,色相绕路会导致动画过程中出现跳变。increasing hue 插值方向固定,适合色相循环动画;decreasing 则适合反向色相动画。

三步下一步

第一步:审计现有渐变。 翻出所有 linear-gradient、radial-gradient、conic-gradient,找那些从高饱和色到高饱和色的组合。如果中段发灰,先把颜色换成 oklch 格式,再用 in oklch 重写。

第二步:建立设计系统颜色基准。 用 color-mix + oklch 建立派生色体系,一个主色派生 9 个梯度,渐变统一走 oklch 插值。这样换主题色时只需改一个变量。

第三步:加 @supports 渐进增强。 92% 覆盖率已经很高,但 Safari 16.1 以下和部分 Android 旧版浏览器不支持颜色空间插值关键字:

.gradient {
  background: linear-gradient(135deg, #7b5cff, #00d4ff); /* fallback */
}
@supports (background: linear-gradient(in oklch, red, blue)) {
  .gradient {
    background: linear-gradient(135deg in oklch, oklch(55% 0.22 265), oklch(70% 0.18 200));
  }
}

CSS 颜色系统这几年变化很大,从 color-mix() 到相对颜色语法再到插值空间控制,实际上在把原来必须在设计软件里做的颜色逻辑一步一步搬到浏览器里。渐变是视觉感知最敏感的地方之一,在这个环节用对插值空间,视觉效果的提升是立竿见影的。

评论区

0 条评论

登录后可评论。