Skip to content

现代 CSS 颜色使用指南 #398

@mqyqingfeng

Description

@mqyqingfeng

1. 前言

大多数开发者处理颜色时,就是从设��稿里复制粘贴一个色值,然后就完事了。

但是!CSS 颜色在过去几年里发生了很多变化!

不仅从 Web 十六进制代码演变成了 hsl() 函数,而且就连你熟知的 rgb() 函数也跟以前不同了!

不信我们接着往下看。

2. 现代 CSS 写法

2.1. 你不需要添加 a

以前,我们用 rgb() 填写普通的 RGB 颜色值,要想改变不透明度,就必须使用 rgba()

.red {
  color: rgb(255, 0, 0);
}

.red-50 {
  color: rgba(255, 0, 0, 0.5);
}

现在,你可以直接添加第 4 个通道了:

.red {
  color: rgb(255, 0, 0);
}

.red-50 {
  color: rgb(255, 0, 0, 0.5);
}

而且,不用担心浏览器兼容问题,只要你不用支持 IE。

2.2. 空格分隔语法

除此之外,逗号现在也被视为老语法了,对于新的颜色函数,我们甚至不能再使用逗号,只能使用新的空格语法。

.red {
  color: rgb(255 0 0);
}

.blue {
  color: hsl(226 100% 50%);
}

不过,使用空格分隔语法时要注意:你不能为 alpha 通道添加第四个值。

换句话说,这样写 color: rgb(255 0 0 0.5) 是不可以的。

如果你要添加第 4 个值,你需要在字母值之前使用一个正斜杠:

.red {
  color: rgb(255 0 0);
}

/* 50% 透明 */
.red-50 {
  color: rgb(255 0 0 / 0.5);
}

.hsl-red-50 {
  color: hsl(0 100% 50% / 0.5);
}

为什么要这么做呢?

因为 CSS 新增了很多颜色函数,统一用斜杠可以快速区分“颜色值”和“透明度”,看代码时一目了然。

2.3. hsl() 也变了

单位现在可选了:

.red {
  color: hsl(0deg 100% 50%);
}

.also-red {
  color: hsl(0deg 100 50);
}

.another-red {
  color: hsl(0 100% 50%);
}

.this-is-red-too {
  color: hsl(0 100 50);
}

小提示: 最好还是保留百分号,因为 VS Code 只有带百分号的才���显示颜色预览。

3. 相对颜色

相对颜色是什么?

简单说就是基于现有颜色生成新颜色。

3.1. 基础用法

我们从一个简单的例子开始:

.rgb-red {
  color: rgb(from #ff0000 r g b);
}

这行代码的意思是:

基于 #ff0000 这个颜色,提取它的红(r)、绿(g)、蓝(b)值,然后用这些值创建一个新颜色。

结果其实就是 rgb(255 0 0)

看起来很傻对吧?但重点是下面这个:

.rgb-red {
  color: rgb(from #ff0000 r g b);
}

/* 轻松创建 50% 透明度版本 */
.rgb-red-50 {
  color: rgb(from #ff0000 r g b / 0.5);
}

3.2. 最实用的场景:处理 CSS 变量

以前你想让一个 CSS 变量颜色变透明,得这样:

:root {
  --color-primary: #2563eb;
  --color-primary-transparent: rgba(37, 99, 235, 0.75); /* 手动转换,麻烦! */
}

现在呢,你可以直接这样写:

:root {
  --color-primary: #2563eb;
}

.semi-transparent-primary-background {
  /* 直接基于变量创建透明版本 */
  background-color: hsl(from var(--color-primary) h s l / 0.75);
}

就像你有一张照片,以前想调整透明度要重新处理一遍,现在滤镜一点就搞定。

3.3. 快速生成配色方案

我们可以快速创建基础颜色的浅色和深色版本:

:root {
  --base: hsl(217 73% 50%);
  --base-light: hsl(from var(--base) h s 75%); /* 调亮 */
  --base-dark: hsl(from var(--base) h s 25%); /* 调暗 */
}

比如实现一个 Toast,它可能基于一种基础颜色,然后创建一种较深的颜色用于文本,一种较浅的颜色用于背景,还需要一个不透明度较低的颜色用于阴影。

以前要 4 个值,现在直接 1 个值搞定:

.toast {
  --toast-color: #222;

  /* 深色文字 */
  color: hsl(from var(--toast-color) h s 15%);

  /* 原色边框 */
  border: 2px solid var(--toast-color);

  /* 浅色背景 */
  background: hsl(from var(--toast-color) h s 90%);

  /* 半透明阴影 */
  box-shadow: 0 12px 12px -8px hsl(from var(--toast-color) h s l / 0.325);
}

此时换颜色也超简单:

[data-toast="info"] {
  --toast-color: #0362fc; /* 蓝色 */
}

[data-toast="error"] {
  --toast-color: hsl(0 100% 50%); /* 红色 */
}

一个变量搞定所有颜色变体,优雅!十分优雅!

4. 浅暗主题切换

4.1. 以前的痛苦

不知道你是否实现过网站的浅色和深色主题:

:root {
  /* 默认浅色主题 */
  --text-heading: #000;
  --text-body: #212121;
  --surface: #efefef;

  @media (prefers-color-scheme: dark) {
    /* 暗色主题 - 第一遍 */
    --text-heading: #fff;
    --text-body: #efefef;
    --surface: #212121;
  }
}

.dark-theme {
  /* 暗色主题 - 又写一遍! */
  --text-heading: #fff;
  --text-body: #efefef;
  --surface: #212121;
}

同样的颜色写两遍,一个给媒体查询(自动切换),一个给切换按钮。

改一次要改两个地方,烦死了!

4.2. 现在的解决方案:light-dark()

:root {
  /* 跟随系统偏好 */
  color-scheme: light dark;

  /* 一次定义,自动切换 */
  --text-heading: light-dark(#000, #fff);
  --text-body: light-dark(#212121, #efefef);
  --surface: light-dark(#efefef, #212121);
}

light-dark(浅色, 暗色) 就这么简单!系统是浅色就用第一个,暗色就用第二个。

4.3. 添加手动切换按钮

如果让用户手动切换主题:

:root {
  /* 默认跟随系统 */
  color-scheme: light dark;
  --text-heading: light-dark(#000, #fff);
  --text-body: light-dark(#212121, #efefef);
  --surface: light-dark(#efefef, #212121);
}

/* 用户选了浅色,锁定 */
html[data-theme="light"] {
  color-scheme: light;
}

/* 用户选了暗色,锁定 */
html[data-theme="dark"] {
  color-scheme: dark;
}

4.4. 组件精细控制

如果某个区域必须保持固定颜色,比如某个背景图上必须是白字:

.hero {
  /* 不管全局主题怎么变,这里永远是浅色主题 */
  color-scheme: light;
  background: url("light-background.webp");
}

5. 渐变优化

5.1. 以前的痛苦

让我们直接看例子,这是一个从蓝到红的渐变:

.gradient {
  --color-1: hsl(219 76 41); /* 蓝色 */
  --color-2: hsl(357 68 53); /* 红色 */
  background: linear-gradient(90deg, var(--color-1), var(--color-2));
}

中间会出现灰扑扑的颜色,不好看:

以前你只能手动加个中间色:

.better {
  --middle: hsl(271 52 41); /* 紫色 */
  background: linear-gradient(90deg, var(--color-1), var(--middle), /* 加一个中间色 */ var(--color-2));
}

是不是好看多了:

但是麻烦呀!我甚至可能需要添加两到三个额外的色阶,以确保它与原设计完全相同。

5.2. 现在的解决办法:指定颜色空间

现在你可以轻松解决,只需要指定一个颜色空间:

.better {
  /* 用 oklch 颜色空间插值,中间色更鲜艳 */
  background: linear-gradient(in oklch 90deg, var(--color-1), var(--color-2));
}

就像拍照时选滤镜,不同的颜色空间会产生不同的中间色效果。oklch 在大多数情况下效果最好。

看下效果对比:

唯一的真正问题是,不同的颜色空间可能更适合不同的渐变,所以有时确实需要花点时间摸索。

可选的颜色空间有:

  • oklch / lch
  • oklab / lab
  • hwb
  • xyz
  • 不指定默认是 srgb

5.3. 实现彩虹渐变

以前做彩虹要指定每一个颜色。

现在只需要:

.rainbow {
  /* 从红色绕一整圈再回到红色,走长路径 */
  background: linear-gradient(in hsl longer hue 90deg, red, red);
}

longer hue 的意思是“走远路”,这样就能经过所有颜色了。

实现效果如下:

6. 超宽色域——当客户就要那个色

有时候客户会拿着他们的 Logo 说:“我就要这个色,一模一样的!”

问题是,普通的 hex、rgb()hsl() 用的是 sRGB 色域,能表示的颜色有限。

这就像以前电视只能显示几百种颜色,然而现在的手机屏幕能显示几百万种。

6.1. 解决方案:display-p3

为了满足客户的需求,你可以使用 color颜色函数,并使用 display-p3 色域。

.vibrant-green {
  /* 使用 display-p3 色域,颜色更鲜艳 */
  color: color(display-p3 0 1 0);
}

如果浏览器不支持,会自动回退到能显示的最接近颜色,不会出错。

你可以在 Chrome 的开发者工具查看这种颜色:

点击色块,它会显示你选择的颜色,但同时也会显示使用 sRGB 色域的显示器的色域限制。

建议: 除非客户真的非常在意那个特定颜色,一般用不着。但知道有这个方案总是好的。

7. 总结

现在你可以更轻松地写代码了:

  1. 少打字 - 不用区分 rgbargb,用空格代替逗号
  2. 少定义变量 - 用相对颜色基于一个颜色生成多个变体
  3. 少写重复代码 - light-dark() 一次定义两套主题
  4. 更好的渐变 - 指定颜色空间让中间色更漂亮
  5. 更精确的颜色 - 需要时可以用更宽的色域

最重要的是: 这些特性浏览器支持都很���了(除了 IE,但谁还在乎 IE 呢?)

我是冴羽,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