给公司的组件库补深色模式时,我接手过一份只写 HEX 的色值规范。设计同学要求"hover 态就是主色深一点点",听起来是半句话的事,我却在那个蓝色上反复对不齐:手工把 #1E90FF 换算成 HSL 时饱和度算成了 88%,改完亮度再转回 HEX,已经变成另一个颜色。同一颗按钮在设计稿和网页上差了一档,评审时被当成"还原度问题"来回扯了两天。颜色格式本身不难,难的是三套写法之间来回换算时人手最容易出错的那几步。
一、三种写法描述的是同一个东西,只是坐标轴不同
HEX 和 RGB 是同一套"红绿蓝三通道"的两种记法,HSL 换了一组更贴近直觉的轴:色相、饱和度、亮度。同一种蓝的四种写法放在一起是这样:
| 格式 | 写法 | 这一种蓝的取值 | 分量范围 | 什么时候用它 |
|---|---|---|---|---|
| HEX | #RRGGBB | #1E90FF | 每两位 00–FF | 设计稿标注、CSS 变量、品牌色文档 |
| RGB | rgb(r,g,b) | rgb(30,144,255) | 各通道 0–255 | 需要参与计算、和 JS 打交道 |
| HSL | hsl(h,s%,l%) | hsl(210,100%,56%) | H 0–360,S/L 0–100% | 派生深浅、做主题色梯度 |
| RGBA | rgba(r,g,b,a) | rgba(30,144,255,0.5) | A 为 0–1 | 蒙层、悬浮态半透明 |
主题色体系里 HSL 更省事的原因就在这:只动 L,H 和 S 保持不变,得到的就是同色相的深浅一档。上面那个蓝把亮度从 56% 压到 46%,得到约 rgb(0,117,235),也就是接近 #0075EB 的深色,正好用作 hover。
二、手算为什么会错:两个公式的必经中间量
HEX 转 RGB 是无脑的,两位十六进制查表即可:1E=30、90=144、FF=255。问题出在 RGB 到 HSL,必须先拿到最大值和最小值:
- 亮度
L = (max + min) / 2。那个蓝里 max=255、min=30,(255+30)/2/255 ≈ 56%。 - 饱和度
S = (max − min) / (1 − |2L − 1|),分母里的|2L−1|是所有人漏掉的那一项。我当初直接用了(max−min)/max,算出 88%,而正确值是 100%。 - 色相要按"哪个通道最大"分三段选公式,算出来还可能是负数,得加 360 再取模。这一步手算几乎必错。
反方向从 HSL 回 HEX 还要再经过 chroma 和中间量,两套公式互推一次就是十几次乘除。颜色这件事上,手算的性价比极低——我现在都是在颜色转换里输入任一种格式,让三种值同时出来,再复制需要的那种。
三、带透明度时最坑的一点:8 位 HEX 的字节顺序
CSS 里的 8 位十六进制是 #RRGGBBAA,透明度在最后:rgba(30,144,255,0.5) 写成 HEX 是 #1E90FF80(0.5 × 255 ≈ 128 = 80)。但 Android 的颜色整型、不少设计插件和图片格式用的是 #AARRGGBB,透明度在最前。同一个数值挪了位置,读出来就是完全另一个颜色:把 #1E90FF80 当成 #AARRGGBB 解析,透明度取的是 0x1E,只有约 12%,剩下的三个字节被读成 rgb(144,255,128)——本来该是半透明蓝的蒙层,结果变成一层几乎看不见的浅绿。我那次半透明蒙层在 iOS 端一片怪色,就是栽在这个顺序上。跨端传色值时,宁可多写一行注释标明通道顺序。
四、色值定了之后,还要看它和背景能不能读清
WCAG 对正文与背景要求的对比度是 4.5:1,大号文字放宽到 3:1。拿 #1E90FF 配纯白算,相对亮度约 0.27,对比度约 3.2:1——在白底上放小字其实不合格;换成压暗一档的 #0075EB,对比度升到约 4.4:1,仍差一点点,得再把 L 往下调几档。这类判断不需要仪器,需要的是别把"好看"当成"看得清"。
还有两个会偷偷改变色值的环节:一是取色来源,屏幕截图取色时如果图片带 ICC 配置或做过压缩,读到的 RGB 会和设计稿差 1–3 个单位;二是显示器,普通办公屏覆盖 sRGB 不足时,饱和度高的绿和蓝会明显偏灰。规范落地时我更倾向固定 HEX,HSL 只作为派生工具使用。
五、常见疑问
Q:#fff 这种三位写法是什么?
是 #ffffff 的缩写,规则是每一位都恰好重复两次才行。#1E90FF 不能缩写,#FF00AA 可以写成 #F0A。
Q:HSL 里的 H 为什么到 360 又回到红色?
因为色相是一个圆:0 红、60 黄、120 绿、180 青、240 蓝、300 品红,360 与 0 重合。做同色系派生时让 H 加减十几度就是邻近色,加 180 度是互补色。
Q:L 调到 0 和 100 会发生什么?
L=0 是纯黑、L=100 是纯白,此时 S 和 H 都不再影响结果。所以"给一个色加亮"调到 90% 以上就会开始失去色相,深色模式里的文字色通常停在 70%–90% 之间。
Q:主题变量到底存 HEX 还是 HSL?
看你要改哪个维度。只换深浅、做亮暗两套主题,存 HSL 更好算;只做整体替换品牌色、且要和设计稿一一对齐,存 HEX 更直观。两者可以无损互转,不必担心后期改不回来。
Q:从界面截图里取的色为什么和规范对不上?
截图链路会做色彩管理和压缩,取到的值可能有 1–3 的偏差。要精确值时优先从设计文件里的标注或 CSS 变量本身取,再顺手换算成需要的格式。
现在那个组件库的规范是两层:HEX 作为唯一权威值,HSL 只用来生成状态色并在文档里附一行换算结果。当初那两天的来回,本质上是把"深一点"这种模糊描述直接交给了两种坐标系——把换算变成一次输入输出的固定动作之后,这类分歧就不再需要开会解决了。