现代 CSS 颜色使用指南进阶篇

431 阅读9分钟

在上篇文章,我们学会了现代 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. 实用技巧

掌握了基础用法,我们就可以讲些实际开发中会用到的场景了。

  1. 创建互补色(色轮对面的颜色):
:root {
  --color-primary: #2563eb; /* 蓝色 */

  /* 色相加 180 度,跳到对面 */
  --color-secondary: hsl(from var(--color-primary) calc(h + 180) s l);
}
  1. 创建三色组合:
: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 点是配色,完美对称!

使用效果如下:

  1. 相对调整
: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 srgbin oklab 等,未来会默认用 oklab

不同颜色空间混出来的结果不一样:

一般推荐:

  1. 先试 oklab
  2. 再试 oklch
  3. 不满意就换其他的

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. 总结

最后总结下本篇文章的重点:

  1. 用 calc() 操纵颜色 - 数学生成配色方案
  2. OKLCH - 基于人眼感知的颜色系统,不同色相亮度一致
  3. 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 干货。