约 7 分钟读完 · 更新于 2026-10-21 · 文中每条都能在下面的配套工具里动手验证
这是最常见的一类「色差 bug」:Figma 里复制出 #4f8cff,写进代码要改成变量就转成 hsl(219,100%,65%),之后某次「顺手优化」又把它转回 hex,得到的却是 #4d8bff。设计稿与线上肉眼看起来差不多,但设计系统的色板比对会把它们判成两个不同的颜色。真正的差别不在格式,在于 HEX 是「设备上那一格像素的绝对数值」,而 HSL 是「按色相饱和度明度重新算出来的近似描述」,来回换算必然要经过一次量化与舍入。
挑错格式是色差问题的上游来源。判据只有一条:这份数值是「最终写死的颜色」还是「要继续派生的基色」。
| 格式 | 取值范围 | 典型写法 | 谁在用 |
|---|---|---|---|
| HEX | #RGB / #RRGGBB / #RRGGBBAA | #4f8cff | Figma、Git diff、CSS 变量 |
| RGB / rgba() | 通道 0–255,alpha 0–1 | rgba(79,140,255,.5) | Canvas、ImageData、JS 计算 |
| HSL / hsla() | H 0–360,S 与 L 0–100%,alpha 0–1 | hsl(219,100%,65%) | CSS 主题变量、渐变插值 |
| HSV / HSB | H 0–360,S 与 V 0–100% | hsv(219,69%,100%) | Figma 色轮、PS 取色器;CSS 无此写法 |
const hex2rgb = h => {
h = h.replace("#", "");
if (h.length === 3 || h.length === 4) h = [...h].map(c => c + c).join("");
const n = parseInt(h, 16);
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
};
const rgb2hsl = ([r, g, b]) => {
[r, g, b] = [r / 255, g / 255, b / 255];
const mx = Math.max(r, g, b), mn = Math.min(r, g, b), d = mx - mn;
let h = 0, s = 0; const l = (mx + mn) / 2;
if (d) {
s = d / (1 - Math.abs(2 * l - 1));
if (mx === r) h = 60 * (((g - b) / d) % 6);
else if (mx === g) h = 60 * ((b - r) / d + 2);
else h = 60 * ((r - g) / d + 4);
if (h < 0) h += 360;
}
return [Math.round(h), Math.round(s * 100), Math.round(l * 100)];
};
const rgb2hsv = ([r, g, b]) => {
[r, g, b] = [r / 255, g / 255, b / 255];
const mx = Math.max(r, g, b), mn = Math.min(r, g, b), d = mx - mn;
let h = 0;
if (d) {
if (mx === r) h = 60 * (((g - b) / d) % 6);
else if (mx === g) h = 60 * ((b - r) / d + 2);
else h = 60 * ((r - g) / d + 4);
if (h < 0) h += 360;
}
return [Math.round(h), Math.round(mx ? (d / mx) * 100 : 0), Math.round(mx * 100)];
};
rgb2hsl(hex2rgb("#4f8cff")); // [219, 100, 65] → 回到 hex 是 #4d8bffsRGB 每个通道只有 256 级,HEX 是它的十六进制写法;HSL 则是把 RGB 换算到柱坐标系后的结果,换算过程要除法、要乘 360、还要按百分比取整,每一步都在引入误差。所以只有 #ffffff 和 #000000 这类极值能保证无损往返,中间色几乎都会漂。下面这组是从真实项目里取的样本,可以直接拿去说服坚持「用 HSL 就不掉色」的人。
| 原色(唯一真源) | HSL 表达 | 转回来的 HEX | 偏差 |
|---|---|---|---|
| #4f8cff | hsl(219,100%,65%) | #4d8bff | 绿通道 +11,肉眼可见 |
| #ec4899 | hsl(330,81%,60%) | #ec4699 | 绿通道 −2,轻微偏紫 |
| #10b981 | hsl(160,84%,39%) | #10b77f | 绿通道 −10,饱和度掉了 |
| #0ea5e9 | hsl(199,89%,48%) | #0da2e7 | 绿通道 −3,红通道 −1 |
| #f59e0b | hsl(38,92%,50%) | #f59f0a | 仅末位差 1,可接受 |
| #7f7f7f | hsl(0,0%,50%) | #808080 | 灰阶最容易翻车 |
| #64748b | hsl(215,16%,47%) | #65758b | 三通道全偏,最差的一例 |
| #ffffff | hsl(0,0%,100%) | #ffffff | 无损 |
| #000000 | hsl(0,0%,0%) | #000000 | 无损 |
代码写的是 sRGB,屏幕不一定按 sRGB 显示。下面这六种「代码完全正确但颜色不对」的情况,按实际排查频率排序。
| 现象 | 真正的原因 | 怎么确认与修 |
|---|---|---|
| 同一 HEX 在笔记本和手机屏上颜色不同 | 色域不同:广色域屏按 P3 显示,代码写的是 sRGB | 用支持 P3 的浏览器对比;保守做法是退回更饱和的 sRGB 值,或显式写 color(display-p3 ...) |
| PNG 截图贴进页面偏色 | 图片带 ICC 配置文件,浏览器按色彩管理做了转换 | 用 <a class="faq-link" href="/tools/image-convert/">图片格式转换</a>重存一遍去掉配置文件 |
| 半透明色块换到深色卡片上就变了 | alpha 是与背后颜色实时合成的,背景变了结果就变 | 先把两层合成成不透明色再评估,别拿原色值判断 |
| 设计稿与网页差一档饱和度 | HSV 的 S 与 HSL 的 S 不是同一个量,同色相下取值不同 | 以 HSL 数值为准重新取样,并在代码里只维护一份 HSL |
| hex → hsl → hex 之后色偏了 | 8 位通道量化加取整,往返本身就不是无损的 | 把 HEX 定为唯一真源,HSL 只用于派生色阶,不再回写 |
| 每次刷新颜色都不一样 | 色相被随机化,或 HSL 里 hue 用了随机值 | 把 hue 限定在品牌色相 ±30° 内,其余通道固定 |
CSS 的 hsla() 与 rgba() 里 alpha 取 0 到 1,SVG 的 fill-opacity 取 0 到 1,HSL 字符串用逗号写法(hsl(219,100%,65%,.5))与空格写法(hsl(219 100% 65% / .5))都合法,后者可读性更好且不受旧浏览器解析歧义影响。8 位 hex(#RRGGBBAA)看着方便,实际有两个坑:一是把 alpha 误当成百分比填(比如 #4f8cff32 以为是 32%),二是老工具链与部分设计软件导出时不认这种写法。写法统一比选哪种更重要——一个项目里只用一种。
因为 HEX 是像素通道的绝对值,HSL 是重新推导的近似表示,往返必然经过一次量化与舍入。实测 #4f8cff 经 hsl(219,100%,65%) 转回得 #4d8bff,绿通道差 11,肉眼可辨。修法是把 HEX 定为唯一真源,HSL 只用来派生色阶、不再回写。用 颜色转换器把两种数值并排看一眼最直观。
CSS 里只有 HSL,没有 HSV;HSV 只存在于 Figma、Photoshop 这类设计工具里。做网页样式用 HSL,需要从 Figma 取色时理解两者差异即可:同一色相下 HSV 的 S 与 HSL 的 S 不是一个量,色板对不上时以 HSL 为准。
按三步:① 确认色值本身是否一致(hex 字符串大小写、三位与六位写法都等价,rgba 逗号与空格写法影响解析);② 确认设备色域——广色域屏按 P3 显示,sRGB 值会显得偏灰;③ 排除色彩管理,PNG/JPG 若带 ICC 配置文件,浏览器会做转换。
都能,但别混用。alpha 在 CSS 中取 0 到 1,8 位 hex 的最后两位是十六进制的 0–255,很多人在这里把 32 误读成 32%。项目里统一用一种写法更省心;若色值来自 Figma 导出、需要频繁往返,优先用 rgb(通道 / alpha) 的形式,透明度与颜色分离,最不容易出错。