仅用CSS指定每主题颜色的几种方法
摘要
本文介绍了五种仅用CSS实现每主题颜色(浅色/深色/自动)的技术,无需JavaScript,利用prefers-color-scheme、:has()、CSS变量和color-mix()等特性。
<p><a href="https://lobste.rs/s/budvjr/few_ways_specifying_per_theme_colours">评论</a></p>
查看缓存全文
缓存时间: 2026/05/16 11:11
# 仅用CSS指定每主题颜色的几种方式 —— Chris Morgan
来源:https://chrismorgan.info/css-themed-colours 🗓️2026\-05\-16•标签:/css (https://chrismorgan.info/css),/meta=also (https://chrismorgan.info/meta%3Dalso)
我在搭建这个网站时考虑到了这个问题。实际上是因为我忘了 `light-dark()`;如果早些想起它,我可能就不会折腾出这么多花样了!
**我的要求:**(可能与你的要求不符)
- 必须支持自动(基于 `prefers-color-scheme`)、浅色和深色三种模式,通过单选按钮选择。
- 必须无需JavaScript即可工作(持久化超出范围)。
## 假设的基础HTML和CSS
具体如何包含和结构化由你决定,但后续所有示例都基于类似这样的结构:
```html
主题
跟随系统
浅色
深色
```
我将假设以下CSS特性可用:
1. `@media (prefers-color-scheme: dark)` 用于自动选择。该特性于2019–2020年推出,通常已足够。
2. `:has()` 用于手动选择,无需额外JavaScript。它较新,支持回溯到2023-12 (https://caniuse.com/css-has);*我*认为它已足够可靠,但如果你不满意,有两种选择:
- 只支持一种行为(通常是自动),无JS。这是非常合理的选择。你可以为根元素添加 `light` 或 `dark` 类。
- 将单选按钮改为body的直接子元素,并隐藏它们视觉上,然后使用 `#theme-foo:checked ~ * ...` 代替 `:root:has(#theme-foo:checked) ...`。这比较麻烦,会有一些后果和不便,但通常是可行的。
3. 本文在某些地方也会使用嵌套选择器(2023-12);但如果你愿意,可以很容易地展平。
接下来是五种技术。
## 1. 硬着头皮全部写出来
老派且冗长。
- 兼容性最好。
- 易于扩展到浅色和深色以外的主题。
- 语法冗长。
- 深色主题的值必须重复。
```css
some-element {
color: darkred;
:root:has(#theme-dark:checked) & {
color: pink;
}
@media (prefers-color-scheme: dark) {
:root:has(#theme-auto:checked) & {
color: pink;
}
}
}
```
通常会有不同的结构,嵌套方式实际上像下一个示例那样反转,但主旨相同。
## 2. 大量颜色调色板变量
可能是最流行的方法,传统上如此。不过我很惊讶地发现这个实现直到2014–2017年才落地。感觉远不止那么久。但称之为“传统”仍然合理,因为在此之前人们经常使用Sass变量达到类似效果(那时你很少有多主题)。
- 自2017年4月起可用。
- 易于扩展到浅色和深色以外的主题。
- 使用站点的语法很理想。
- 将颜色定义与使用位置分离可能有益也可能有害,可能适合也可能不适合你的项目。它*可能*鼓励一些坏模式和好模式,并且可能会增加一些开销。(我故意说得模糊,这里不展开。)
- 深色主题的值必须重复。
声明部分:
```css
:root {
--color-somepurpose: darkred;
--color-another: ...;
⋮
}
:root:has(#theme-dark:checked) {
--color-somepurpose: pink;
--color-another: ...;
⋮
}
@media (prefers-color-scheme: dark) {
:root:has(#theme-auto:checked) {
--color-somepurpose: pink;
--color-another: ...;
⋮
}
}
```
添加更多主题很简单。使用位置:
```css
some-element {
color: var(--color-somepurpose);
}
```
显然,你也可以将变量用于接下来的三种方法。
## 3. 使用 `color-mix()` 配合每个主题一个变量
我不记得之前见过这种技术(实际上我对`color-mix()`受到的关注之少感到惊讶),但它有效。基本思想是:将颜色定义为所有主题颜色的混合,但用一个变量将其中一个主题设为100%,其余设为0%。(实践中通常只使用两个主题:浅色和深色。)
- 自2023年5月起可用。
- 易于扩展到浅色和深色以外的主题。
- 语法相对紧凑。
- 语法可能不常见,但相当直接。遗憾的是目前必需插值方法部分。
- 为实际*混合*颜色/主题开辟了有趣的可能性。`--dark: 50%;` 是什么意思?你可以尝试后果!
- 如果你尝试混合颜色和主题,控制插值会非常*痛苦*;你必须弄清楚如何*数学地*表达所需的函数。
这就是我为自己网站采用的技术,因为一旦我意识到潜力,我就想尝试混合主题。我已经开始写更多关于它的内容。(https://chrismorgan.info/interpolating-themes) 它比你想象的更有趣/更复杂。
在样式表开头:
```css
:root {
--dark: 0%;
}
:root:has(#theme-dark:checked) {
--dark: 100%;
}
@media (prefers-color-scheme: dark) {
:root:has(#theme-auto:checked) {
--dark: 100%;
}
}
```
然后对每种颜色这样使用 `color-mix()`:
```css
some-element {
color: color-mix(in oklab, darkred, pink var(--dark));
}
```
这样在浅色模式下是 `darkred`,深色模式下是 `pink`。大多数情况下你只关心改变*颜色*,但如果你想根据主题改变其他东西,可以将变量设为数字而不是百分比,并使用 `calc()`。(没有考虑过后果——我希望百分比和数字统一,`100%` 等同于 `1`。)
### 语法兼容性陷阱
注意规范 (https://drafts.csswg.org/css-color-5/#color-mix) 允许的内容和 MDN (https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/color_value/color-mix) 介绍的内容:
1. 插值方法是可选的,默认是 `oklab`,但这只是2025年末 (https://github.com/w3c/csswg-drafts/issues/10484#issuecomment-3201053810) 才发生的。所有主流浏览器现已支持,但按照我的定义,要到大约一年半后才能放心依赖。所以相当一段时间内,你仍然需要写 `in oklab,`。MDN的浏览器兼容数据缺少这项。(https://github.com/mdn/browser-compat-data/issues/28693) Lightning CSS缺少这项。(https://github.com/parcel-bundler/lightningcss/issues/1224)
2. 规范允许*一个或多个*颜色规范,这有利于混合超过两个主题,但大多数实现仍只支持恰好两个。这个问题*确实*在MDN浏览器兼容数据中跟踪了。(https://caniuse.com/mdn-css_types_color_color-mix_variadic_color_arguments) Lightning CSS缺少它。撰写本文时,只有Firefox提供了这个功能(150,2026-04-21)。
### 超过两个主题
你可以通过设置更多变量和混合更多颜色来扩展到两个以上主题。定义两个新主题 Grass 和 Ocean:
```css
:root {
--grass: 0%;
--ocean: 0%;
}
:root:has(#theme-grass:checked) {
--grass: 100%;
}
:root:has(#theme-ocean:checked) {
--ocean: 100%;
}
```
然后使用它们:
```css
some-element {
color: color-mix(in oklab, color-mix(in oklab, color-mix(in oklab, darkred, pink var(--dark)), green var(--grass)), blue var(--ocean));
}
```
希望在2028年(或一旦Lightning CSS或类似工具支持),你可以简化为以下形式(并且对0%到100%之间的值效果也更好):
```css
color: color-mix(in oklab, darkred, pink var(--dark), green var(--grass), blue var(--ocean));
```
## 4. `light-dark()`
- 自2024年5月起可用(Safari 17.5)。这*勉强*满足我的首选兼容性要求:“Safari两年,其余一年半”。
- 不能用于浅色和深色以外的更多或其他主题。
- 语法紧凑清晰。
首先,你*必须*正确设置 `color-scheme`。所有技术*都*应该这样做,原因包括滚动条和表单元素着色,但对于 `light-dark()` 来说,这是它*根本*工作的必要条件。
```css
:root {
color-scheme: light dark;
}
:root:has(#theme-light:checked) {
color-scheme: light;
}
:root:has(#theme-dark:checked) {
color-scheme: dark;
}
```
然后:
```css
some-element {
color: light-dark(darkred, pink);
}
```
## 5. `if()`
- 仅Chromium系列实现(2025-05);Firefox或Safari尚未支持。
- 易于扩展到浅色和深色以外的主题。
- 语法很大程度上符合程序员的期望,
- ...但相当冗长。
在样式表开头:
```css
:root {
--theme: light;
}
:root:has(#theme-dark:checked) {
--theme: dark;
}
@media (prefers-color-scheme: dark) {
:root:has(#theme-auto:checked) {
--theme: dark;
}
}
```
然后对每种颜色使用:
```css
some-element {
color: if(
style(--theme: light): darkred;
style(--theme: dark): pink;
);
}
```
这比 `color-mix()` 灵活性不同:`color-mix()` 仅限于颜色,且允许连续插值;`if()` 适用于所有类型,但仅限于离散值。根据已有示例,添加更多主题很简单。
## 5.(实际为第六种)暂停的 `@keyframes` 动画
这是一种我偶尔见过的技巧,但我不记得曾见过用于*颜色*。你大概不定义颜色调色板*变量*,而是定义颜色调色板*关键帧规则*(每个使用它的属性一个!),其中“from”表示浅色模式,“to”表示深色模式。然后在使用位置添加一个暂停的动画,在深色模式下应用负延迟将动画置于“to”(深色)值。
- 人们会认为你疯了。他们很有可能是对的。
- 自2020年1月起可用(当Edge采用Chromium时;否则就是2013年7月)。
- 与 `color-mix()` 一样,该技术*引诱*你允许混合颜色/主题!
- 如果你真的想混合主题,对于控制插值,没有比这更好的了(几乎就像是为它设计的!)(不过……关键帧并不*总是*比数学函数好。)
- 除非你没有其他动画,否则你将被大量重复和极其痛苦的 `animation` 声明组合所困扰。
- 我打赌你会在途中遇到特异性风格的麻烦,因为动画甚至比 `!important` 更强大。
```css
@keyframes color-somepurpose {
from { color: darkred; }
to { color: pink; }
}
some-element {
animation: 1s paused color-somepurpose forwards;
:root:has(#theme-dark:checked) & {
animation-delay: -1s;
}
@media (prefers-color-scheme: dark) {
:root:has(#theme-auto:checked) & {
animation-delay: -1s;
}
}
}
```
我不再进一步解释这一点;如果你不理解,可能最好如此——或者将其视为挑战。
最后,作为一个总结,这里有一个比较表格。
| 技术 | 支持时间 | 选择器重复 | 美观/可维护性? | 插值 | 主题 | 使用它暗示什么 |
|------|----------|------------|----------------|------|------|---------------|
| 手动 | 永远 | 到处 | 笨拙 | 否 | 任意 | 你*只有*一个主题。 |
| 变量† | 2017-04 | 仅在定义处 | 除了定义重复外,使用体验很好 | 否 | 任意 | 你相当正常,但不够*时尚*。 |
| `color-mix()` | 2023-05 | 否 | 不熟悉,暂时有点冗长 | 是,但非线性时麻烦 | 任意 | 你可能玩得太开心了。 |
| `light-dark()` | 2024-05 | 否 | 典范 | 否 | 仅浅色/深色 | 你很时尚。 |
| `if()` | 仅Chromium | 否 | 清晰但冗长 | 否 | 任意 | 你*完全*不在乎网页的健康。 |
| `@keyframes` | 2020-01 | 远比那糟糕 | 这个单元格太小,无法充分描述噩梦 | 近乎终极灵活性 | 理论上任意(但请别尝试) | 病态天才?那是我善意解读的极限了。 |
† 变量应用于手动方式,但你可以将它与 `color-mix()`、`light-dark()` 和 `if()` 一起使用,而且很可能会这样做。`@keyframes` 有点已经包含了自己的替代方案,而且可能比听起来更糟糕。
相似文章
算法主题引擎
本文介绍了新的CSS `contrast-color()`函数,该函数允许开发人员自动选择黑色或白色文本,以实现与任何背景颜色的可访问对比度,解决网络上长期存在的低对比度问题,而无需依赖JavaScript。
深色模式的六个层次(2024)
一篇探讨网页设计中深色模式实现的六个层次的文章,范围从基础的meta标签方法到使用CSS的高级配色方案切换技术。
使用Web标准实现深色模式
一篇关于使用Web标准(如color-scheme元标签和prefers-color-scheme媒体查询)在网站上实现深色模式的指南,并包含一个JavaScript切换开关,可尊重用户偏好并将其存储在localStorage中。
使用CSS生成颜色
本教程介绍如何使用CSS的oklch()函数和相对颜色功能,从单个十六进制代码动态生成完整的调色板,从而简化网站的颜色管理。
使用 Temporal 和 CSS color-mix 实现基于时间的背景颜色过渡
本文演示了如何使用 Temporal API 检测本地时间,并结合 CSS color-mix 在个人网站上实现根据一天中的时间变化背景颜色过渡的效果。