「主色的 5%」这句话里,5% 没有单位
这句话的问题不在数字,在单位。指的是不透明度,就是用白色稀释主色;指的是 HSL 饱和度,是另一件事;指的是亮度偏移,是第三件事。三种做法算出来的颜色,视觉强度可以差三倍以上,而交接文档里写的都是「5%」。
更麻烦的是多数设计工具显示的饱和度是 HSL 口径。HSL 的饱和度定义成「相对当前明度的纯度」,明度越接近 100,它越不管用。下面这组数字来自库内 100 套配色的背景色,两种口径给出的结论方向相反。
同一批底色,两个口径差四倍
| 判定口径 | 判定为「带色」 | 得到的结论 |
|---|---|---|
| HSL 饱和度 > 12 | 38 套 | 近四成配色的底是带色的 |
| Lab 色度 C* ≥ 2 | 9 套 | 不到一成看得出来 |
把这 38 套按色度重算,中位只有 1.03。这个数值意味着什么:把它的颜色和纯白并排放,绝大多数人分不出来。而它们的 HSL 饱和度中位是 9.1,其中还有几套写着 100。
全库的色度分布更极端:C* 小于 1 的有 75 套(75%),1 到 2 之间有 16 套(16%),达到 2 的只有 9 套(9%)。中位数 0.63。
也就是说,一个标着「带一点主色」的底色,九成情况下用户看到的就是白色。这不是坏事,但如果设计意图是「看得出分区」,那这个意图没有实现。
为什么 HSL 饱和度在浅色区会失效
HSL 的 S 是相对量:它描述颜色离开中性轴多远,而这个距离在明度逼近白点时被压缩。当 L 到 98,RGB 三个通道都挤在 250 以上,最大值与最小值只差个位数,饱和度再高也没有空间可以表现。
用同一个色相 h=30 做一组演示,每一行固定明度,把饱和度从 30 拉到 90:
| 明度 L | 色度 C*(依次为 S=30 / 60 / 90) |
|---|---|
| 98% | 1.1 / 1.8 / 2.9 |
| 95% | 2.5 / 4.6 / 7.1 |
| 90% | 4.8 / 9.9 / 14.5 |
表格右侧每行三个数字是色度 C*,依次对应 S=30 / 60 / 90。看两件事。
第一,L=98 时把饱和度从 30 拉到 90,色度只从 1.1 到 2.9,仍然在「看不出来」的区间。饱和度翻了三倍,视觉上几乎没有变化。
第二,什么都不改,只把明度从 98 降到 90,S=30 的色度就从 1.1 涨到 4.8,超过 L=98 时 S=90 的四倍。换句话说,浅色区的带色强度主要由明度决定,饱和度是次要变量。想要「加一点点色」,往下调明度比往上加饱和度有效得多。
同一个 5%,两种实现差三百多倍
这句话真正的危险在于:两种都自称「按主色的 5% 来」的实现,结果根本不在一个量级。用库内一个真实主色 #2563eb 各做一次:
| 实现 | 得到的颜色 | 色度 C* |
|---|---|---|
| A:主色 5% 不透明度,叠在白底上 | #f4f7fe | 3.72 |
| B:饱和度取主色的 5%,明度提到 98 | #fafafa | 0.01 |
A 落在「看得出带色」的窗口里,可以直接用。B 是 #fafafa,色度 0.01,和纯灰没有区别,做了等于没做。两者相差 323 倍。
这不是谁对谁错的问题。A 碰巧好用,是因为用白色稀释在降低饱和度的同时也抬高了明度,两个变量一起动;B 只把饱和度降下来、明度原地不动,于是什么也没发生。同一个百分比、同一句需求,一个落在窗口内,一个落在窗口外,而设计稿上写的是同一行字。
能用的窗口
按库内实测和上面的演示,色度可以分成四档,每一档对应一个具体用途:
| 色度 C* | 观感 | 适合放哪里 |
|---|---|---|
| < 1 | 就是白或灰,换掉也不会被察觉 | 想要纯白就直说,不必绕成「主色的 2%」 |
| 1 ~ 2 | 说不出哪里不一样,但换掉能感觉到 | 大面积页面底 |
| 2 ~ 4 | 看得出是带色的,但仍算克制 | 分区底、卡片底、表头 |
| > 4 | 明确有颜色,不再是「一点点」 | 只适合小面积,或者深色底 |
库内达到 2 的 9 套里,最高的是 #0f0d1a,色度 8.4,但它是一套深色底。浅色区里最高的一套不到 7,而且已经在「明显带色」那一档。这说明浅色区能用的窗口很窄:2 到 4 之间。
深色底是另一回事
库内 2 套深色底的色度是 7.0 到 8.4,比浅色区的上限还高,但看起来并不刺眼。原因还是明度:深色区域里人眼对色彩的分辨力更弱,同样的绝对色差要用更大的色度才等价,所以深色底反而容纳得下更多颜色。
另一组对照能说明同一件事:库内正文文字色的色度中位是 4.6,是背景色中位 0.63 的七倍多。文字是界面上面积最小、最必须被读到的元素,它的色度必须够强;底色是最大面积、最不必被注意的元素,它的色度必须够弱。同一套规则在两端给出相反的数值。
所以「底色的色度」没有统一答案,它取决于面积。深色底、小面积、需要被注意,都往高走;浅色底、大面积、需要退到后面,都往低走。
在项目里怎么落地
- 用色度判断,不要用 HSL 饱和度。C* = √(a² + b²),在 CIELAB(D65 白点)下算。任何颜色库都有现成实现,二十行代码。HSL 的饱和度只在中间明度区间可以粗略当指标用,而底色恰好不在那个区间。
- 浅色底想要「看得出带色但不抢眼」,把明度压在 95 到 97、色度定在 2 到 3。这两个数字是互相关联的:明度越低,同样的色度越明显。先定明度,再调色度。
- 交接时给绝对色值,不给百分比。「主色 5%」到实现端会变成三四种不同的颜色,因为每个人对 5% 的理解不一样。给
#f7f8fa,并且注明它是色度 1.8 的中性偏蓝底,没有歧义。
第三条最容易被跳过,但它决定前两条有没有用。一个能被复现的数值,比一个听起来精确的百分比可靠。
数据怎么来的
上面每一个数字都由构建期的 src/lib/guide-stats.mjs 从库内 100 套配色的 bg 字段现算,CIELAB 转换在 src/lib/lab.mjs,用 D65 白点与 sRGB 标准矩阵。色度分档的阈值(1 / 2 / 4)是在这套数据上定的,不是引用某个标准:没有标准规定「底色该有多少色度」,只能从实际分布里找窗口。
演示表格里的九个色度值由公式在构建期生成,色相固定 h=30,不依赖具体配色,任何时候重算都是同一组数字。
相关:卡片与分割线为什么看不见讲的是另一类「数值很低但确实存在」的角色;编辑标准记录了三道闸门与计算口径。