OKLCH 与现代 CSS 颜色 — 为什么你的配色方案在某些浏览器中看起来异常
RGB 和 HSL 是为阴极射线管显示器设计的。OKLCH 修复了感知均匀性,color-mix() 改变了渐变混合,而相对颜色语法无需预处理器即可启用。
你为主要按钮选择了蓝色,在浏览器中看起来很合适。你在一台较新的MacBook Pro上打开预发布环境,看起来……尚可,只是略显平淡。你的设计师从iPhone上发送了一张截图,同样的按钮看起来明显更鲜艳。相同的十六进制代码,不同的显示效果。
这是P3色域的差距。实际上,这是你一直使用的CSS颜色模型失效的最不重要部分。更大的问题是HSL背后的数学原理。
sRGB色域的上限
每一个你曾经写过的十六进制颜色和 rgb() 你曾经写过的值都存在于sRGB色域中——这是一种1996年为CRT显示器设计的标准。sRGB覆盖了人类可见色域的大约35%。自2017年起,所有iPhone都使用P3色域,自2016年起大多数MacBook也采用了P3色域,其覆盖范围约为45%。
当你写 #3b82f6时,该颜色被限制在sRGB范围内。在P3显示设备上,浏览器理论上可以渲染更饱和的蓝色——但你的CSS并未要求这一点。你默认忽略了显示器的潜力。
要直接访问P3颜色,你需要使用 color(display-p3 0.2 0.4 0.9)。这可行。但这是OKLCH重要的原因。更大的问题是HSL的核心假设。
HSL的亮度是谎言
HSL相比十六进制确实有实质性的改进。色相、饱和度、亮度——概念上合理,人类可编辑。问题在于,“L”并不像你想象的那样意味着亮度。
将黄色置于 hsl(60, 100%, 50%) ,将蓝色置于 hsl(240, 100%, 50%) 并并列。两者都恰好处于50%的亮度。
.yellow { background: hsl(60, 100%, 50%); }
.blue { background: hsl(240, 100%, 50%); }
黄色看起来几乎像白色,蓝色看起来是中等偏暗。它们的亮度感知完全不同。这是HSL的感知非均匀性——L轴并不对应人类视觉对亮度的处理方式。
这在构建颜色尺度时会产生真实维护问题。如果你通过在HSL中逐步增加L值来生成一个9级的色阶,你的黄色和青色会显得过曝,而蓝色和紫色则显得浑浊。你最终不得不手动调整数值,这完全违背了系统化方法的初衷。
OKLCH:专为感知设计
OKLCH由Björn Ottosson于2020年设计。它基于1970年代的CIELAB(一种感知色域空间),但修正了其已知的色相偏移问题——特别是紫色/蓝色问题,即调整色度时会明显改变感知色相。
三个通道:
- L(亮度) ——从0到1,感知上均匀。黄色中L为0.5的亮度与蓝色中L为0.5的亮度实际相同。
- C(色度) ——从0(灰色)开始,增加到大约0.3-0.4,对应高度饱和的颜色。没有固定上限——上限随色相和目标色域而变化。
- H(色相) ——从0到360度,大致相当于HSL的色相。
以下是Tailwind蓝色500在两种系统中的表现:
/* sRGB hex */
background: #3b82f6;
/* OKLCH equivalent — same color, different notation */
background: oklch(0.623 0.214 259.1);
/* Push chroma past sRGB — more vivid blue on P3 displays, clips gracefully on sRGB */
background: oklch(0.623 0.27 259.1);
最后一个值超出了sRGB色域。在支持P3的浏览器中,会渲染更鲜艳的版本;较旧的浏览器则会裁剪到最接近的sRGB等效值。实现渐进式增强,无需额外努力。
color-mix() 和为什么色域会影响混合效果
color-mix() 在Chrome 111、Firefox 113和Safari 16.2中推出。它按比例混合两种颜色——简单明了。但你指定的色域会显著影响结果。
/* Mixes through sRGB — midpoint is a washed-out brown-grey */
background: color-mix(in srgb, oklch(0.7 0.2 30), oklch(0.7 0.2 270));
/* Mixes through the OKLCH color wheel — midpoint is a vivid purple */
background: color-mix(in oklch, oklch(0.7 0.2 30), oklch(0.7 0.2 270));
当你在sRGB中混合橙色和蓝色时,你是在平均原始通道值。中间点会经过一种去饱和的棕灰色。而在OKLCH中,你是在色轮上进行插值——你最终会落在一个鲜艳的紫色上。哪种结果正确取决于意图,但对于UI过渡、渐变和悬停状态,OKLCH的插值几乎总是你想要的结果。
一个实际用例:无需硬编码第二个十六进制值即可生成禁用状态。
.button--disabled {
background: color-mix(in oklch, var(--color-primary) 40%, white);
cursor: not-allowed;
}
相对颜色语法
相对颜色语法(Chrome 119+、Firefox 128+、Safari 16.4+)允许你通过修改现有颜色的各个通道来推导出新颜色:
:root {
--brand: oklch(0.623 0.214 259.1);
}
.button:hover {
/* Lighten by 8% lightness units */
background: oklch(from var(--brand) calc(l + 0.08) c h);
}
.button:active {
/* Darken and reduce chroma slightly */
background: oklch(from var(--brand) calc(l - 0.08) calc(c - 0.02) h);
}
.button--muted {
/* Drop chroma to near-zero: perceptual grey at the same lightness */
background: oklch(from var(--brand) l 0.03 h);
}
这取代了Sass的 lighten(), darken(),并且 desaturate() ,使用原生CSS——无需预处理器,无需构建步骤,无需同步多个十六进制值。而且由于你使用的是OKLCH,“变亮”在你色板中的每个色相上都会真正显得更亮,而不仅仅是在部分色相上。
一个真正的局限性:复杂的通道约束(如限制色度以保持在色域内)需要冗长的 calc() 链,这些链会迅速变得复杂。对于简单的亮度和色度调整,语法简洁明了。对于更复杂的情况,预先计算值并生成CSS自定义属性更为可维护。
2026年的浏览器支持
| 特征 | 铬合金 | Firefox | Safari |
|---|---|---|---|
oklch() | 111+ | 113+ | 15.4+ |
color-mix() | 111+ | 113+ | 16.2+ |
| 相对颜色语法 | 119+ | 128+ | 16.4+ |
color(display-p3) | 111+ | 113+ | 10+ |
对 oklch() 的全球支持在2026年中已超过90%。少数支持者是较旧的基于Chromium的企业浏览器以及仍在运行Firefox 113以下版本的系统。如果这些浏览器在你的用户群体中存在,只需两行备用代码即可。
.button {
background: #3b82f6; /* sRGB fallback */
}
@supports (background: oklch(0 0 0)) {
.button {
background: oklch(0.623 0.214 259.1);
}
}
在实际应用中:如果你的分析确认Chrome 111+、Firefox 113+和Safari 15.4+,你可以完全跳过备用代码。该 @supports 包装器是为了在内部工具中提供安心,而你无法控制浏览器版本。
转换你现有的配色方案
OKLCH采用过程中的主要障碍是从你现有的十六进制值转换为OKLCH坐标,并理解每个颜色的通道含义。该 统一色彩空间转换器 可双向转换——粘贴一个十六进制或HSL值,得到OKLCH输出,然后从真实参考点开始调整L和C。它支持HSL、RGB、Lab、LCH、HSV和OKLCH,当你从Figma文件获取十六进制值并需要转换为可理解的OKLCH值时,这一点非常有用。
如何实际迁移
你不必一次性重写所有内容。一个实用的步骤序列:
- 从交互式颜色开始 ——你的主要、次要和破坏性令牌。这些是你已经为悬停、激活和禁用状态生成变体的颜色。OKLCH与相对颜色语法立即用原生CSS替代了这些逻辑。
- 暂时保留静态颜色使用十六进制 ——文本、背景、边框。这些颜色不会被程序化操作,因此目前转换它们并无收益。
- 从零开始构建下一个新系统,使用OKLCH ——这才是感知均匀性带来的结构性优势。你的中性色阶的500级将真正处于视觉中间点,而所有色相的饱和色将保持一致,无需手动调整。
目标不是为了OKLCH本身。而是拥有一个数学模型与人眼所见一致的颜色系统——这样你不再每次需要稍微更浅的蓝色时都需手动补偿。
