渐变为什么看起来发灰发脏:色带与 OKLCH 修复

· 约 4 分钟 🌈 CSS 渐变

设计稿配色挺鲜艳,到了 CSS 里写成 linear-gradient 就发脏发灰——这是 CSS 渐变最常见的”翻车”。原因不在配色,在浏览器默认的插值空间。

为什么会发灰

CSS 渐变默认在 sRGB 空间做插值——也就是把两端的 RGB 值线性混合。

background: linear-gradient(90deg, #0000ff, #ffff00);

(0, 0, 255) 到黄 (255, 255, 0),中点 (127, 127, 127)——纯灰。眼睛看到的就是从蓝慢慢褪色到灰,再变成黄。

互补色对(蓝/黄、红/青、品红/绿)几乎一定中间灰。即使不是互补,只要色相跨越大,中段也会比两端”暗一截”。

OKLCH 怎么解决

oklch() 是 2023 年标准化的感知均匀色彩空间。它把颜色拆成 L(亮度)、C(彩度)、H(色相),亮度对人眼线性。

background: linear-gradient(in oklch, #0000ff, #ffff00);

加一个 in oklch,浏览器在 OKLCH 空间走色相弧线,绕开灰色。蓝→黄会经过紫红或青绿,但不再经过灰——视觉上连续、有彩度。

可选的还有:

颜色空间特点何时用
in srgb(默认)数学正确,视觉差几乎不要用
in oklab感知均匀,直线插值同色相区间最佳
in oklch感知均匀,色相走弧线跨色相、彩色渐变最佳
in hsl老方案,亮度不均匀不推荐
in display-p3广色域,更鲜艳知道目标设备支持时

简单结论:只要不是同一色相的明暗变化,统统用 in oklch

色相走哪边

跨色相时浏览器默认走”短弧线”。但有时你想要走长的(比如做一圈完整色相轮):

/* 短弧线:红 → 黄经过橙 */
linear-gradient(in oklch shorter hue, red, yellow)

/* 长弧线:红 → 黄经过品红、蓝、青、绿 */
linear-gradient(in oklch longer hue, red, yellow)

/* 强制顺时针 / 逆时针 */
linear-gradient(in oklch increasing hue, red, yellow)
linear-gradient(in oklch decreasing hue, red, yellow)

conic-gradient 做色环时几乎一定要写 longer hue,否则两端会被走最短路径合并成一段。

色带是另一个问题

修了颜色空间还能看到一圈一圈的”等高线”——这是 8-bit 量化色带,跟颜色空间无关。在大面积浅色渐变上特别明显。

修法是叠一层极淡噪点:

.banner {
  background:
    /* 1.5% 不透明噪点 */
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence baseFrequency='0.9' numOctaves='2'/><feColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.04 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>"),
    linear-gradient(in oklch, #ff7e5f, #feb47b);
}

噪点不透明度 3% 以下,眼睛看不到颗粒,但色带被打散。Spotify、Stripe、Apple 大量用这招。

径向渐变的”中心点”陷阱

background: radial-gradient(circle at 50% 50%, #ff7e5f, #feb47b);

radial-gradient 默认椭圆延伸到最近角,容器宽高比变化时形状会跑——做卡片背景看起来还行,做按钮就容易拉成怪椭圆。明确写 circlecircle 200px 更稳。

至于 at 50% 50%:如果中心点不在容器内(比如 at -20% 50% 做一边亮一边暗),可见区域只有渐变的”一段”,颜色会看起来比预期暗——把第二个色标位置从 100% 改成 60% 就能让暗色范围限制住。

圆锥渐变做色环

background: conic-gradient(
  in oklch longer hue,
  red, yellow, lime, cyan, blue, magenta, red
);

这是把 OKLCH 的 H 维度可视化的最简单方式。头尾都写 red 才会闭合,不然 360° 处会有一道接缝。

调试技巧

判断哪一段在哪个空间走得对,最快的方法是给同一个容器叠两层渐变做对比:

.compare {
  background:
    linear-gradient(in oklch, #0066ff, #ffcc00) top / 100% 50% no-repeat,
    linear-gradient(#0066ff, #ffcc00) bottom / 100% 50% no-repeat;
}

上半 OKLCH,下半 sRGB,差距一目了然。

常用配色清单

实测在 OKLCH 下不糊的几对:

主题起 / 止
Sunset#ff7e5f#feb47b
Ocean#2193b0#6dd5ed
Aurora#a8edea#fed6e3
Cosmic#fc466b#3f5efb
Mint#11998e#38ef7d

这些在 sRGB 下也基本不发灰(同色相邻近),但加 in oklch 会让中段更明亮。

一句话总结

颜色发灰加 in oklch,色带加噪点;它们是两件事,要分开治。

❓ 常见问题

为什么蓝→黄的渐变中间是灰的?

这是 sRGB 默认插值在数学上的"正确"结果。蓝色和黄色在 RGB 空间是对角,连线穿过 (0.5, 0.5, 0.5) 附近——也就是灰色。在 sRGB 里直接 mix 互补色,几乎一定会经过一段灰。换成感知均匀的颜色空间(OKLCH / OKLAB / HSL)就不会经过灰色,而是绕开走一条更亮的路径。

linear-gradient(in oklch, ...) 浏览器都支持了吗?

主流浏览器从 2023 年中起陆续上车——Safari 16.2、Chrome 111、Firefox 113。到 2026 年所有主流版本都已稳定,可以直接用,不需要降级。如果要兼容老 Edge / 老 Safari,把同一对色标也写成普通 linear-gradient(...) 放在前面做兜底即可。

色带(banding)和"中间发灰"是同一个问题吗?

不是。色带是 8-bit sRGB 阶梯太粗,眼睛能看出"一圈一圈",跟颜色空间无关——任何 8-bit 渐变在足够大的面积上都会出现。修法是叠加一层 1-3% 噪点打散阶梯。发灰是中间值的色相问题,根因是颜色空间,OKLCH 能根治。两个问题可以同时存在。

conic-gradient 也能用 OKLCH 吗?

可以,语法一样:conic-gradient(in oklch, ...)。但 conic 在色相切片上特别容易出现"亮度跳变"——尤其是经过红/青互补处。in oklch longer hue / shorter hue 可以显式指定色相走哪一边,强烈推荐用在 conic 上调色相轮。

🌈 打开 CSS 渐变 linear/radial/conic·色标拖拽·角度调节·预设·一键复制 CSS