使用Web标准实现深色模式
摘要
一篇关于使用Web标准(如color-scheme元标签和prefers-color-scheme媒体查询)在网站上实现深色模式的指南,并包含一个JavaScript切换开关,可尊重用户偏好并将其存储在localStorage中。
<p><a href="https://lobste.rs/s/d1hevp/dark_mode_with_web_standards">评论</a></p>
查看缓存全文
缓存时间: 2026/07/05 10:29
# 使用 Web 标准实现深色模式
来源:https://olliewilliams.xyz/blog/dark-mode/
尊重用户的操作系统设置非常简单:在 CSS 中使用 `prefers-color-scheme` 媒体查询即可。不过,这或许还不够:用户还应该能够在每个网站自行定制选择。例如,用户可能希望应用界面使用深色模式,但在内容密集的网站上阅读长文时则偏好浅色模式。我们需要做到以下两点:
1. 当用户首次访问网站时,以系统设置为默认方案。
2. 允许用户通过应用内的切换控件覆盖系统设置。
网页的颜色方案可以通过文档 `<head>` 中的 HTML meta 标签设置,也可以通过 CSS 的 `color-scheme` 属性作用于 `html` 元素。在慢速连接下,CSS 加载可能需要时间,因此推荐使用 meta 标签。
当用户第一次访问你的网站时,通过设置 `<meta name="color-scheme" content="light dark">` 来尊重用户的系统偏好。
若要通过网页应用或站点中的控件覆盖操作系统设置,可使用 JavaScript 更新 `content` 属性的值:设为 `light` 强制浅色模式,设为 `dark` 强制深色模式,设回 `light dark` 则恢复为系统设置。
```js
const metaTag = document.querySelector('[name="color-scheme"]');
const savedScheme = localStorage.getItem("colorScheme");
if (savedScheme) {metaTag.setAttribute('content', savedScheme);}
btnlight.addEventListener('click', function() {
metaTag.setAttribute('content', 'light');
localStorage.setItem("colorScheme", "light");
});
btndark.addEventListener('click', function() {
metaTag.setAttribute('content', 'dark');
localStorage.setItem("colorScheme", "dark");
});
btnsystem.addEventListener('click', function() {
metaTag.setAttribute('content', 'light dark');
localStorage.removeItem("colorScheme");
});
```
#### `color-scheme` 会影响哪些内容?
- 通过 `light-dark()` CSS 函数设置的颜色、渐变或图片。
- 系统颜色,如 `Canvas` 和 `CanvasText`。
- 滚动条颜色。
- HTML 元素(如按钮)的默认颜色。
- iframe 样式(只要 iframe 文档通过 meta 标签选择启用)。
- 使用了 `light-dark()` 或 `prefers-color-scheme` 的 SVG。
#### `color-scheme` 不会影响哪些内容?
`color-scheme` 与 `prefers-color-scheme` 媒体查询之间存在一个不太好的脱节。`prefers-color-scheme` 反映的是操作系统设置——无论 `color-scheme` 的值如何。如果你在页面内提供了一个实现深色模式的切换开关,那么你就无法采用 `prefers-color-scheme` 媒体查询。以下代码不会受到 `color-scheme` 的影响:
```css
@media (prefers-color-scheme: dark) {
.element {
background-color: black;
color: white;
}
}
```
除了使用 `background-image` 之外,遗憾的是,并没有一个与 `<meta>` 元素等效、能引用 `color-scheme` 的方法。
有两个例外情况,`color-scheme` 会影响 `prefers-color-scheme` 媒体查询:
- iframe
- SVG
下面是两个展示相同文档的 iframe。iframe 内部的文档使用了 `prefers-color-scheme` 媒体查询。
```html
<iframe src="..."></iframe>
<iframe src="..." style="color-scheme: dark;"></iframe>
```
如以上示例所示,当父文档使用了 `color-scheme: dark` 时,`@media (prefers-color-scheme: dark)` 样式会被应用。同样的原理也适用于 SVG。下面是一个 `.svg` 文件的内容:
```svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="40" />
</svg>
```
```css
circle { fill: rgb(40,40,40); }
@media (prefers-color-scheme: dark) { circle { fill: rgb(216, 216, 216); } }
```
CSS 规范最近已进行更新([链接](https://github.com/w3c/csswg-drafts/pull/13857#:~:text=This%20PR%20applies%20the%20resolution%20from%20the%20recent%20F2F%20in%20#13377%2C%20making%20(prefers%2Dcolor%2Dscheme)%20reflect%20the%20effects%20of%20%3Cmeta%20name=color%2Dscheme%3E)),使得文档的 `color-scheme` 在所有上下文中都会影响媒体查询,但目前还没有浏览器实现这一改动。
#### Safari 的一些注意事项
- 在 SVG 中支持 `prefers-color-scheme` 媒体查询的功能已添加到 **Safari 27**([链接](https://developer.apple.com/documentation/safari-release-notes/safari-27-release-notes?changes=la,la#:~:text=175598175)-,Fixed%20an%20issue%20where,the%20system%20color%20appearance,-.%20(176413340)),但 `color-scheme` 不会影响该媒体查询(参见 bug 报告:[链接](https://bugs.webkit.org/show_bug.cgi?id=316640))。
- 在 iframe 中支持 `prefers-color-scheme` 媒体查询的功能已添加到 **Safari 27**([链接](https://developer.apple.com/documentation/safari-release-notes/safari-27-release-notes?changes=la,la#:~:text=140674753)-,Fixed%20an%20issue%20where,was%20set%20to%20dark,-.%20(142072593)),并且父文档的 `color-scheme` 会正确覆盖它,🎉。但其他 bug 仍然存在(参见 bug 报告:[链接](https://bugs.webkit.org/show_bug.cgi?id=316680))。
## 在图片和渐变中使用 `light-dark()`
`light-dark()` 函数最初仅限于颜色。现已可用于渐变和图片(自 Chrome/Edge 版本 150、Firefox 版本 150 及 Safari Technology Preview 起)。
```css
.bg-gradient {
background-image: light-dark(linear-gradient(15deg, #b9b6ff, #308dc6), linear-gradient(15deg, #6b7495, #001339));
}
```
```html
<div class="bg-gradient">...</div>
```
还可以根据 `color-scheme` 在单一纯色和渐变之间切换。
```css
.bg-grad-solid {
background-image: light-dark(linear-gradient(15deg, #b9b6ff, #308dc6), image(#001339));
}
```
### 图片
```css
.bg {
background-image: light-dark(url(/lightmode.avif), url(/darkmode.avif));
}
```
## 更改颜色、渐变和图片之外的内容
通常需要在不同模式之间更改的主要是颜色,但也有例外。例如,在深色模式下,`box-shadow` 可能在深色背景上不可见,因此你可能选择改用边框。目前实现这一点相当有挑战性。
CSS 标准组织正计划添加一种方法来检测当前颜色方案([链接](https://github.com/w3c/csswg-drafts/issues/10577#issuecomment-3329616811)),可能通过 CSS 的 `if()` 语句或样式查询来实现,但尚未有浏览器实现此功能。
我们可以拼凑出一种变通方法。下面来看两种方式。
定义一个 CSS 变量,当页面使用深色模式时设为 true:
```css
html {
--dark: false;
}
html:has([content="light dark"]) {
@media (prefers-color-scheme: dark) {
--dark: true;
}
}
html:has([content="dark"]) {
--dark: true;
}
```
现在所有浏览器都支持样式查询,你可以根据自定义属性的值应用样式:
```css
@container style(--dark: false) {
.card {
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.2);
}
}
@container style(--dark: true) {
.card {
border: solid 1px rgb(94, 94, 94);
}
}
```
一个更好的方法同样使用样式查询:
```css
@property --usedScheme {
syntax: "<color>";
inherits: true;
initial-value: transparent;
}
body {
--usedScheme: light-dark(white, black);
}
@container style(--usedScheme: white) {
.card {
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.2);
}
}
@container style(--usedScheme: black) {
.card {
border: solid 1px rgb(94, 94, 94);
}
}
```
## (可能的)未来:通过 JavaScript 覆盖 `prefers-color-scheme`
未来我们或许能通过 JavaScript 来覆盖 `prefers-color-scheme` 媒体查询。目前有相关规范([链接](https://drafts.csswg.org/mediaqueries-5/#script-control-user-prefs))、MDN 条目([链接](https://developer.mozilla.org/en-US/docs/Web/API/User_Preferences_API))和 Chrome Canary 中的原型,但 Safari 团队反对这一想法([链接](https://github.com/WebKit/standards-positions/issues/252))。
相似文章
深色模式的六个层次(2024)
一篇探讨网页设计中深色模式实现的六个层次的文章,范围从基础的meta标签方法到使用CSS的高级配色方案切换技术。
仅用CSS指定每主题颜色的几种方法
本文介绍了五种仅用CSS实现每主题颜色(浅色/深色/自动)的技术,无需JavaScript,利用prefers-color-scheme、:has()、CSS变量和color-mix()等特性。
算法主题引擎
本文介绍了新的CSS `contrast-color()`函数,该函数允许开发人员自动选择黑色或白色文本,以实现与任何背景颜色的可访问对比度,解决网络上长期存在的低对比度问题,而无需依赖JavaScript。
readable.css
readable.css 是一个CSS框架,为网站提供了合理且美观的基础默认样式,支持亮/暗模式、响应式设计和垂直节奏,强调一致性和语义化HTML。
使用 Temporal 和 CSS color-mix 实现基于时间的背景颜色过渡
本文演示了如何使用 Temporal API 检测本地时间,并结合 CSS color-mix 在个人网站上实现根据一天中的时间变化背景颜色过渡的效果。