Skip to content

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

@mqyqingfeng

Description

@mqyqingfeng

在上篇文章,我们学会了现代 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 干货。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions