在上篇文章,我们学会了现代 CSS 颜色的基础用法。
本篇我们进入更高级的领域:如何像设计师一样操纵颜色。
CSS 现在能做的事情,甚至比设计软件还要强大!
1. 操纵颜色
1.1. 基础回顾
先复习一下上篇文章的内容:
:root {
--primary: #ff0000;
}
.primary-bg-50-opacity {
/* h s l 不是字母,是变量! */
background: hsl(from var(--primary) h s l / 0.5);
}
注意: h s l 这三个字母其实是变量,分别存储了色相(hue)、饱和度(saturation)、亮度(lightness)的值。
我们可以替换这些变量,比如给绿色(#00ff00)加点蓝:
/* 基础绿色没有蓝色成分 */
.green-with-a-touch-of-blue {
/* 在蓝色通道加 25 */
color: rgb(from #00ff00 r g calc(b + 25));
}
这就像调色板,在基础的绿色中,添加点“蓝色颜料”。
1.2. 实用技巧
掌握了基础用法,我们就可以讲些实际开发中会用到的场景了。
- 创建互补色(色轮对面的颜色):
:root {
--color-primary: #2563eb; /* 蓝色 */
/* 色相加 180 度,跳到对面 */
--color-secondary: hsl(from var(--color-primary) calc(h + 180) s l);
}
- 创建三色组合:
:root {
--color-primary: #2563eb;
/* 色轮上均匀分布 120 度 */
--color-secondary: hsl(from var(--color-primary) calc(h + 120) s l);
--color-tertiary: hsl(from var(--color-primary) calc(h - 120) s l);
}
就像时钟:12 点是主色,4 点和 8 点是配色,完美对称!
- 相对调整
:root {
--color-primary-base: #2563eb;
/* 比基础色亮 25% */
--color-primary-lighter: hsl(from var(--color-primary-base) h s calc(l + 25));
/* 比基础色暗 25% */
--color-primary-darker: hsl(from var(--color-primary-base) h s calc(l - 25));
}
使用这种方法,不管基础色是多亮,都会相对地变亮或变暗。
这就像调空调,“比现在低 5 度”比“设定到 20 度”更灵活。
2. 暗黑模式的层次感
现在我们能相对调整颜色了,可是究竟有什么用呢?
让我给你举个具体的使用场景。
在浅色主题中,我们可以根据阴影来区分层次:
但在深色背景下,阴影的效果并不明显。
因此在深色主题中,我们希望每个层次的颜色略微变浅。
借助上篇讲过的 light-dark()和相对颜色调整,我们便可以实现:
:root {
--surface-base-light: hsl(240 67% 97%);
--surface-base-dark: hsl(252 21% 9%);
}
/* 第一层:基础层 */
.surface-1 {
background: light-dark(var(--surface-base-light), var(--surface-base-dark));
}
/* 第二层:稍微亮一点 */
.surface-2 {
background: light-dark(var(--surface-base-light), hsl(from var(--surface-base-dark) h s calc(l + 4)));
}
/* 第三层:再亮一点 */
.surface-3 {
background: light-dark(var(--surface-base-light), hsl(from var(--surface-base-dark) h s calc(l + 8)));
}
想象一下深海:越接近水面,光线越充足。暗色模式的层次就是这个原理。
3. 完整配色方案
设计师创建配色方案时,不只是简单地调整亮度,还会同时微调色相和饱和度,比如
- 变亮时:饱和度增加,色相向冷色调偏移
- 变暗时:饱和度降低
这便可以通过 CSS 来实现:
:root {
--primary-base: hsl(221 83% 50%);
/* 400: 亮度60%,色相-3度,饱和度+5% */
--primary-400: hsl(from var(--primary-base) calc(h - 3) calc(s + 5) 60%);
/* 300: 亮度70%,色相-6度,饱和度+10% */
--primary-300: hsl(from var(--primary-base) calc(h - 6) calc(s + 10) 70%);
/* 以此类推... */
}
为什么要这么麻烦呢?
因为只调亮度会让浅色看起来“失去活力”。
就像拍照:自动模式能拍,但手动调整曝光、对比度、色温会更漂亮。
第一排是都调整的版本,第二排是未调整色相和饱和度的版本。
最明显的区别在于 100、200 和 300 这几个色块,当仅调整亮度值时,颜色看起来失去了一些鲜艳度。
4. OKLCH 更科学的配色方式
4.1. 问题:HSL
HSL 虽然很棒,但在使用时,你会发现一些问题,让我们看个例子:
.green-bg {
/* 饱和度 100%,亮度 50% */
background: hsl(100 100% 50%);
color: white;
}
.blue-bg {
/* 同样饱和度 100%,亮度 50% */
background: hsl(220 100% 50%);
color: white;
}
你会发现,色和蓝色的饱和度、亮度虽然完全一样,但视觉效果完全不同:
明显蓝色背景上的文字清晰易读,对比度超过 5,而绿色背景上的文字却很难看清,对比度仅略高于 1。
之所以会这样,是因为 HSL 的“亮度”不符合人眼感知。
人眼对绿色比蓝色更敏感,所以同样的“50%亮度”,绿色看起来更亮。
4.2. 解决:OKLCH
而这就是 oklch() 的优势。
OKLCH 是基于感知亮度设计的:
.consistent-green {
background: oklch(0.54 0.23 261);
}
.consistent-blue {
background: oklch(0.54 0.23 146);
}
使用效果如下:
现在不管什么颜色,亮度值相同,人眼看起来的亮度就相同!
4.3. 讲解:OKLCH 的三个参数
在 LCH 颜色模型中:
第一个值是亮度(Lightness),取值范围为 0 到 1。它的计算方式与 HSL 略有不同,因为它基于感知亮度,但概念相同,即 0 是黑色,1 是白色。
第二个值是色度(Chroma),类似于饱和度,0 是灰色,越大越鲜艳,但最大值不固定,取决于亮度和色相(这是 OKLCH 最麻烦的地方)
第三个值是色相(Hue),和 HSL 一样是色轮,0 到 360 度,区别是 0 度是洋红色(HSL 里 0 度是红色)
OKLCH 的尴尬之处就在于色度(Chroma)的最大值会变:
/* 某些色相+亮度组合,0.4 已经是极限 */
.max-chroma-1 {
background: oklch(0.6 0.4 120);
}
/* 但另一些组合,0.4 可能太高或太低 */
.max-chroma-2 {
background: oklch(0.8 0.4 200); /* 可能超出范围 */
}
就像不同口味的饮料,有的最浓是“3 勺糖”,有的最浓是“5 勺糖”,没有统一标准。
4.4. 实用技巧:结合相对颜色
虽然直接写 OKLCH 值很麻烦,但我们可以用 HSL 定义基础色,然后用 OKLCH 保持一致性:
.toast {
--base-color: hsl(225, 87%, 56%); /* 用熟悉的 HSL 定义 */
}
[data-toast="info"] {
/* 只改变色相,保持亮度和色度 */
--toast-color: oklch(from var(--base-color) l c 275);
}
[data-toast="warning"] {
--toast-color: oklch(from var(--base-color) l c 80);
}
[data-toast="error"] {
--toast-color: oklch(from var(--base-color) l c 35);
}
这样做的好处在于:不同颜色的提示框,边框对比度、整体饱和度感觉都很一致。
5. 混合颜色
有的时候,我们可能需要混合两种颜色:
.purple {
/* 红色和蓝色各占 50% */
color: color-mix(in srgb, red, blue);
}
就像调颜料:红色颜料加蓝色颜料,得到紫色。
目前必须定义一个颜色空间,所以必须写 in srgb 或 in oklab 等,未来会默认用 oklab。
不同颜色空间混出来的结果不一样:
一般推荐:
- 先试
oklab - 再试
oklch - 不满意就换其他的
5.1. 控制混合比例
默认情况下,使用该功能时 color-mix(),每种颜色将使用 50% 的强度。
当然,我们也可以控制特定颜色的具体强度。
/* 90% 红色 + 10% 蓝色 */
.red-with-a-touch-of-blue {
background: color-mix(in oklab, red 90%, blue);
}
/* 或者反过来写 */
.or-like-this {
background: color-mix(in oklab, red, blue 10%);
}
5.2. 创建半透明色
有两种方法可以获得透明的值:
方法一:让总量小于 100%
/* 60% + 20% = 80%,所以透明度是 80% */
.semi-opaque {
background: color-mix(in oklab, red 60%, blue 20%);
}
方法二:混入 transparent
/* 30% 的不透明红色 */
.thirty-percent-opacity-red {
background: color-mix(in oklch, red 30%, transparent);
}
不过如果只是想降低透明度,还是用相对颜色更直接:
.better-way {
background: rgb(from red r g b / 0.3);
}
5.3. 小技巧:分段渐变
/* 不用手动算中间的每个颜色,color-mix 帮你搞定 */
.banded-gradient {
background: linear-gradient(to right, red, color-mix(in oklch, red 75%, blue), color-mix(in oklch, red 50%, blue), color-mix(in oklch, red 25%, blue), blue);
}
6. 未来更简单
重复写这些代码很烦?
CSS 自定义函数快来了:
/* 定义函数 */
@function --lower-opacity(--color, --opacity) {
result: oklch(from var(--color) l c h / var(--opacity));
}
/* 使用函数 */
.lower-opacity-primary {
background: --lower-opacity(var(--primary), 0.5);
}
或者定义整套色阶函数:
@function --shade-100(--color) returns <color> {
result: hsl(from var(--color) calc(h - 12) calc(s + 15) 95%);
}
@function --shade-200(--color) returns <color> {
result: hsl(from var(--color) calc(h - 10) calc(s + 12) 85%);
}
/* 以此类推... */
.call-to-action {
background: --shade-200(var(--accent));
}
.hero {
background: --shade-800(var(--primary));
color: --shade-100(var(--primary));
}
一次定义,到处使用,完美!
7. 总结
最后总结下本篇文章的重点:
- 用 calc() 操纵颜色 - 数学生成配色方案
- OKLCH - 基于人眼感知的颜色系统,不同色相亮度一致
- color-mix() - 像调颜料一样混合颜色
在实际使用时:
- 简单需求:相对颜色 + HSL
- 需要视觉一致性:OKLCH
- 需要混合颜色:color-mix()
- 暗黑模式层次:light-dark() + 相对颜色
本篇整理自《A pragmatic guide to modern CSS colours - part two》,希望能帮助到你。
我是冴羽,10 年笔耕不辍,专注前端领域,更新了 10+ 系列、300+ 篇原创技术文章,翻译过 Svelte、Solid.js、TypeScript 文档,著有小册《Next.js 开发指南》、《Svelte 开发指南》、《Astro 实战指南》。
欢迎围观我的“网页版朋友圈”,关注我的公众号:冴羽(或搜索 yayujs),每天分享前端知识、AI 干货。