刚接触前端开发的时候,有一件事让我很困惑:为什么调整一个颜色要同时改三个数值?我只想把红色调暗一点,但在 RGB 里要改 R、G、B 三个值,而且改完之后颜色往往不是我想要的。
后来我学到了 HSL,才明白问题不在于调颜色这件事本身,而在于 RGB 不是给人用的色彩模型。
RGB 是为机器设计的
RGB 的工作原理很简单:一个像素发出多少红光、多少绿光、多少蓝光。三个通道叠加在一起,形成最终颜色。对显示器来说这是最直接的模型——给它三个电压值,它就能显示了。
但对人来说,RGB 有几个很反直觉的地方。
第一个问题:在 RGB 里,"把颜色调亮"没有单一的操作。你要把 R、G、B 三个值同时增大,但比例不变的话,颜色确实会变亮。不过人的直觉是"我想让这个蓝色变亮一点",而在 RGB 里你要算出蓝色里面 R 和 G 的值(蓝色通道本身就接近 0,所以是改变绿色和红色)。这件事对非设计师来说太抽象了。
第二个问题:RGB 三个通道之间有耦合。你改了一个通道的值,颜色的"色调"可能就变了。假设你要从 rgb(255, 0, 0) 红色过渡到 rgb(255, 128, 0) 橙色,实际上是增加了 G 通道。但如果你不熟悉颜色混合,你不会知道"增加绿色会让红色变橙"。
第三个问题:RGB 不是一个均匀的感知空间。rgb(100, 100, 100) 到 rgb(200, 200, 200) 的灰度变化,人眼看起来不是均匀渐变的——实际需求是在中间插一个伽马校正的步骤。
HSL 的三个维度
HSL 把颜色拆成三个更直观的维度。
色相(Hue):0° 到 360°,绕一圈。0° 是红色,120° 是绿色,240° 是蓝色。中间值对应彩虹色。这个环的设计来自牛顿最早的光色环理论,后来被色彩学不断修正,HSL 沿用了这个模型。
饱和度(Saturation):0% 到 100%。0% 是灰色(无色),100% 是纯色。这个维度描述的是"颜色的鲜艳程度"。
明度(Lightness):0% 到 100%。0% 是完全的黑色,100% 是白色。注意这里的 50% 不是半亮,而是"纯色"——比如 hsl(0, 100%, 50%) 就是纯红色。
这三个维度分开之后,你就可以独立操控它们了。想变得更暗但保持同样颜色?降低 L 值。想让颜色更艳丽但不改变色相?增加 S 值。想做一组同色系的渐变?固定 H 和 S,只改变 L。
这比 RGB 的直觉操作成本低太多了。
HSL 的数学转换
HSL 不是一个跟硬件相关的模型,它是由 RGB 计算出来的。转换公式不复杂,但有几个边界情况需要注意。
从 RGB 到 HSL:
先找到 R、G、B 中的最大值和最小值。然后:
L = (max + min) / 2
如果 max = min:S = 0(无色相)
否则:
if L ≤ 0.5: S = (max - min) / (max + min)
if L > 0.5: S = (max - min) / (2 - max - min)
色相的计算依赖于哪个通道是最大值:
如果 max 是 R: H = (G - B) / (max - min) × 60
如果 max 是 G: H = 2 + (B - R) / (max - min) × 60
如果 max 是 B: H = 4 + (R - G) / (max - min) × 60
而 S = 0 时 H 是未定义的——灰色不分色相。
从 HSL 到 RGB 的转换需要分情况处理饱和度为 0 的情况,以及色相落在哪个扇区。具体公式不算复杂,但有一堆 if-else。
值得注意的是,不同 HSL 实现的公式有细微差别。CSS 标准里用的 HSL 和我刚才写的这个版本完全一致,但很多图形库(比如 Python 的 colorsys 模块)也实现了它。不过 CSS Color Level 4 引入了 hsl() 的改进版,以后可能会取代旧标准。
一个踩坑经历
有一年我做网站的暗黑模式,用 HSL 来调节颜色。我当时的做法是在 CSS 里定义色板变量:
:root {
--primary-h: 210;
--primary-s: 100%;
--primary-l: 50%;
--primary: hsl(var(--primary-h), var(--primary-s), var(--primary-l));
}
然后暗黑模式下直接把 L 值降低:
[data-theme="dark"] {
--primary-l: 35%;
}
看起来挺对的是吧?但实际上暗黑模式不是简单地把颜色调暗。因为暗黑模式的背景是深色(比如 #1a1a1a),你需要在背景上放"相对亮"的前景色才能看清。只是把 L 从 50% 降到 35% 会导致前景色和背景色拉不开对比。
正确的做法是保持前景色的 L 值不变或者甚至提高它,但把饱和度降低来营造"柔和"的观感。这显然违反了"暗黑模式就是降低明度"的直觉。
后来我改成了这样:
[data-theme="dark"] {
--primary-s: 70%;
--primary-l: 60%;
}
饱和度降低(不那么刺眼),明度反而提高(在深色背景上更亮)。这才是暗黑模式正确的调色方向。
HSL 的实际应用
HSL 在设计系统里非常有用。很多现代设计系统(Tailwind、Ant Design、Material UI 等)都内置了基于 HSL 的色板系统。
Tailwind 的色板就是好例子。它的 blue-500 是 hsl(210, 100%, 50%),然后:
- blue-200(更亮): hsl(210, 100%, 85%)
- blue-700(更暗): hsl(210, 100%, 35%)
- blue-900(更暗): hsl(210, 90%, 20%)
看到规律了?H 和 S 几乎不变,只改 L 值。这是 HSL 的核心优势。
在实际开发中,我还发现 HSL 在生成渐变和主题色时特别顺手:
- 互补色方案:H + 180°,S 和 L 不变。快速得到对比色。
- 类似色方案:H ± 30°(或更小的角度),S 和 L 不变。和谐自然。
- 三色方案:H、H+120°、H+240°,等距分布。鲜艳但有风险。
不过 HSL 也有它的缺点。最明显的是:HSL 中的 L=50% 并不是人感知上的"一半亮度"。这个是 CIELAB 等感知均匀色彩模型要解决的问题。如果你在做颜色差异计算(比如算两个颜色的相似度),HSL 就不适合了——直接用 CIEDE2000 公式更好。我这里不做展开,但提醒一句:不要用 HSL 的差值来度量颜色距离,结果跟人眼感知差别很大。
HSL 不同变体,不只是 CSS 里的那个
HSL 有不同的实现。CSS 标准里的 HSL 用的是圆柱体模型,H 用角度表示。但有些领域会用 HSV(也叫 HSB),它和 HSL 的区别在于:
- HSL:L=0% 是全黑,L=100% 是全白,L=50% 是纯色
- HSV:V=0% 是全黑,V=100% 是纯色(不一定是白色),S=0% 时 V 决定灰度
区别很细微,但转换公式不同。Paint 和 Photoshop 的颜色选择器用的是 HSV(HSB),而 CSS 用的是 HSL。如果你从设计软件里取的颜色值直接用在 CSS 里,色值可能是对的,但直觉上的"明度"表现不同。
浏览器里的颜色工具
如果你需要调试颜色,大多数开发者工具的颜色选择器支持在 RGB、HSL、HEX 之间实时切换。在 Safari 的 Web Inspector 里甚至可以预览颜色变量的实际渲染效果。
但日常调色靠手算不现实。颜色转换工具支持 HEX、RGB、HSL 和 CMYK 四种模式的互相转换,还有可视化色彩预览面板——我经常在它上面把设计稿里的 HEX 色值转成 HSL,然后做各种调色操作。
写在最后
HSL 不是什么新技术——它在 1970 年代末就被提出来了。但它真正普及是在 CSS3 把它纳入标准之后。
它解决了 RGB 最核心的问题:颜色应该按人类理解的方式来描述,而不是按硬件工作的方式来描述。色相决定"这是什么颜色",饱和度决定"颜色有多浓",明度决定"颜色有多亮"——这三个参数独立变化,不需要像 RGB 那样猜。
但 HSL 也不是万能的。它跟 RGB 一样不是感知均匀的色彩空间。在做精确的色彩度量(比如计算两个颜色的视觉差异)的时候,还是要用 CIELAB 或者 OKLab。不过大部分前端开发场景下,HSL 已经够用了。