用Web标准实现深色模式
速览
深色模式已成为现代应用的标准配置,但实现方式多样。本文讲解如何使用Web标准(如prefers-color-scheme媒体查询)检测用户系统主题,并动态切换样式。这种方法无需JavaScript,性能更优,且符合标准规范。它有助于提升用户体验并降低屏幕能耗。
AI 深度解读
背景
现代 Web 应用中,暗色模式(Dark Mode)已成为用户体验的核心需求之一。用户期望网站能够自动跟随操作系统的主题设置,同时也能在站点内部手动切换。然而,Web 标准在实现这一功能时存在一些技术细节和兼容性问题。这篇来自 Hacker News 的文章深入探讨了如何利用原生 CSS 和 HTML 标准(而非依赖 JavaScript 框架)来实现暗色模式,并指出了当前实现中的陷阱与未来方向。
核心内容
文章首先强调,尊重用户的操作系统设置是基础:只需在 CSS 中使用 prefers-color-scheme 媒体查询即可。但仅此还不够——用户应当能在每个站点上自定义自己的选择。例如,用户可能希望在应用程序的 UI 中使用暗色模式,但在内容密集的长文阅读网站上使用亮色模式。因此需要做到:首次访问时支持系统设置作为默认值,并在应用中提供一个切换开关,允许用户覆盖系统设置。
网页的颜色方案可以通过两种方式设置:在文档 <head> 中使用 HTML meta 标签 <meta name="color-scheme" content="light dark">,或者在 <html> 元素上设置 CSS 属性 color-scheme。由于慢速连接下 CSS 可能加载延迟,文章推荐使用 meta 标签的方式。首次访问时,<meta name="color-scheme" content="light dark"> 会尊重系统偏好。要覆盖系统设置,则使用 JavaScript 更新该 meta 标签的 content 属性值:light 强制亮色,dark 强制暗色,light dark 恢复为系统设置。示例代码中使用 localStorage 保存用户的选择,并在页面加载时恢复。
color-scheme 会影响什么?
- 通过 CSS 函数
light-dark()设置的色彩、渐变或图片。 - 系统颜色,如
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 媒体查询来切换样式。例如,下面的 <picture> 元素不会受 color-scheme 影响:
<picture>
<source srcset="logo-dark.png" media="(prefers-color-scheme: dark)" />
<img src="logo-light.png" alt="Product logo" />
</picture>
除了使用 background-image,目前没有与 <picture> 元素等价的方法来引用 color-scheme 的值。
但有两个例外情形,color-scheme 会影响 prefers-color-scheme 媒体查询:iframes 和 SVG。在 iframe 中,父文档的 color-scheme 样式可以覆盖子文档的媒体查询(前提是子文档已使用 prefers-color-scheme)。同样,在 SVG 内嵌的 <style> 中,prefers-color-scheme 也会受到包含该 SVG 的文档 color-scheme 的影响。文章给出了两个 iframe 和两个 SVG 示例,展示 color-scheme: light 和 color-scheme: dark 下的不同表现。
CSS 规范最近已更新,使文档的 color-scheme 在所有上下文中都能影响媒体查询,但目前还没有浏览器实现这一变更。
Safari 的注意事项
- SVG 内支持
prefers-color-scheme媒体查询是在 Safari 27 中添加的,但color-scheme不会影响该媒体查询(存在 bug)。 - iframe 内支持
prefers-color-scheme媒体查询也是在 Safari 27 中添加的,父文档的color-scheme会正确覆盖它(🎉)。然而,仍存在其他 bug。
使用 light-dark() 处理图片与渐变
最初 light-dark() 函数仅限用于颜色。现在它也可以用于渐变色和图片(Chrome/Edge 150 版本、Firefox 150 版本、Safari Technology Preview 已支持)。示例:
.bg-gradient {
background-image: light-dark(linear-gradient(15deg, #b9b6ff, #308dc6), linear-gradient(15deg, #6b7495, #001339));
}
甚至可以实现在纯色和渐变之间切换:
.bg-grad-solid {
background-image: light-dark(linear-gradient(15deg, #b9b6ff, #308dc6), image(#001339));
}
图片切换:
.bg {
background-image: light-dark(url(/lightmode.avif), url(/darkmode.avif));
}
改变颜色、图片和渐变之外的内容
大多数情况下,暗色模式仅需改变色彩,但也有例外:例如,一个 box-shadow 在暗色背景上可能不可见,此时可能需要改用 border。实现这样的切换目前较为困难。CSS 标准组织正计划通过 if() 语句或样式查询(style query)来检测当前颜色方案,但尚无浏览器实现。文章提出了两种当前可行的替代方案:
-
定义一个 CSS 自定义属性
--dark来表示是否处于暗色模式,然后利用样式查询(style queries)根据该属性值应用不同样式。例如,构建html:has([content="light dark"])配合prefers-color-scheme: dark来设置--dark: true,或者直接检测content="dark"。然后使用@container style(--dark: false)和@container style(--dark: true)来切换box-shadow和border。 -
更优雅的方式:使用
@property注册一个自定义属性--usedScheme,将其初始值设为transparent,然后在body上通过light-dark(white, black)设定其值。之后通过样式查询检测该属性的实际值(例如white表示亮色,black表示暗色),从而应用不同的样式规则。
未来的可能:通过 JavaScript 覆盖 prefers-color-scheme
未来我们或许能通过 JavaScript 直接覆盖 prefers-color-scheme 媒体查询。已有规范草案、MDN 条目以及 Chrome Canary 的原型,但 Safari 团队反对这个想法。
关键要点
- 基础实现:使用
<meta name="color-scheme" content="light dark">尊重系统设置,并配合 JavaScript 与localStorage提供用户覆盖。 color-scheme与prefers-color-scheme存在脱节:页面内的暗色切换按钮不能依赖prefers-color-scheme媒体查询,因为该查询始终反映 OS 设置。- 例外情形:在 iframe 和 SVG 中,父文档的
color-scheme可以影响子文档的prefers-color-scheme媒体查询。这是当前浏览器实现的特例。 - Safari 在 SVG 和 iframe 中支持
prefers-color-scheme但存在 bug:SVG 内color-scheme不影响媒体查询;iframe 中覆盖正常但仍有其他问题。 light-dark()函数已可用于渐变和图片(Chrome 150+、Firefox 150+、Safari TP),实现图片/渐变在亮暗模式间的自动切换。- 对于非颜色属性的切换(如
box-shadow改为border),当前可通过 style queries + 自定义属性(如--dark或--usedScheme)实现变通方案。 - 未来可能通过 JavaScript 直接覆盖
prefers-color-scheme媒体查询,但浏览器阵营意见不统一(
