深色模式切换开关

深色模式切换开关可让用户为一个网站替换系统配色方案。 通过更新 <meta name="color-scheme"> 来应用替换:light dark 表示“遵循系统偏好设置”,light 或 dark 表示固定明确的配色方案。 颜色 (light-dark())、系统颜色和原生界面都取决于 CSS color-scheme 属性,因此该切换开关通过使该属性与 <meta> 保持同步来发挥作用(请参阅在 CSS 中反映替换)。

是永久性切换开关、设置控件,还是根本没有切换开关?

大多数网站不需要在网站界面(页眉、导航栏、页脚等)中提供永久性配色方案切换开关。 默认情况下,遵循系统偏好设置;如果确实需要手动替换,请将控件放在单独的设置界面中:设置页面、面板或叠加层。

将配色方案固定到操作系统是一种合理但罕见的意图,而罕见的意图应该隐藏在渐进式披露机制之后,而不是放在始终可见的控件中。

持久切换开关主要出现在面向开发者或与开发者相关的网站(例如面向设计师的网站)上,在这些网站上,切换配色方案的任务比普通 Web 用户要频繁得多。 请勿仅仅因为开发者网站上常见就添加此类控件:开发者对此类控件的兴趣并不代表典型用户的兴趣。

实现

首先,按照深色模式中的所有步骤操作,以支持这两种配色方案,并默认使用系统偏好设置。

数据模型

深色模式切换开关的数据模型通常包含三种状态:

  1. 系统(无存储值):界面遵循系统配色方案。理想情况下,这应该是默认设置。
  2. 浅色(存储为 "light"):无论系统配色方案如何,界面都使用浅色主题。
  3. 深色(存储为 "dark"):无论系统配色方案如何,界面都使用深色主题。

此状态通常存储在 localStorage 中,并反映在文档标头中的 <meta name="color-scheme"> 元素中。

在 CSS 中反映替换

<meta> 只是一个呈现提示:作者 CSS 中的任何 color-scheme 声明都会替换它。 由于深色模式会在 :root 上设置 color-scheme: light dark,因此仅更新 <meta> 是无效操作,CSS 优先。 将 <meta> 映射回属性,以便级联遵循该属性:

:root {
  color-scheme: light dark;

  &:has(> head > meta[name="color-scheme"][content="dark"]) {
    color-scheme: dark;
  }
  &:has(> head > meta[name="color-scheme"][content="light"]) {
    color-scheme: light;
  }
}

请勿将 color-scheme: light 或 color-scheme: dark 硬编码为根默认值;基本声明必须保持为 light dark。 CSS 不得依赖于 JS:如果 JS 永远不运行,<meta> 会保持 light dark,网站会遵循系统偏好设置,不会出现任何问题。

持久性和 FOUC 防范

将替换设置保留在 localStorage 中;在恢复为系统默认设置时移除该条目。

为避免出现错误的配色方案,请在紧跟 <meta> 元素之后的内嵌脚本(而非 type=module,也非 defer)中应用存储的值:

<meta name="color-scheme" content="light dark">
<script>
document.querySelector('meta[name="color-scheme"]').content = localStorage.getItem("color-scheme") ?? "light dark";
</script>
  • 此内嵌脚本仅用于防止 FOUC。尽量保持其体积小巧;用于处理配色方案切换的额外 JS 可以稍后加载。
  • 在极少数情况下,localStorage 访问可能会抛出错误(例如,网站数据被屏蔽),但由于此脚本不执行其他操作,因此唯一的副作用是控制台错误。如果这很重要,请用 try .. catch 封装。确保主题仍应用于页面,即使无法持久保存。
  • 替换项可能会从另一个标签页发生更改:请处理 window 的 storage 事件以保持同步。
  • 当系统偏好设置更改为与存储的值一致时,请勿使用 matchMedia() 移除存储的值。 许多用户的操作系统会根据一天中的时间自动切换配色方案;如果每当两者恰好一致时就移除存储的值,则根本无法固定配色方案。

针对 HTML 和非颜色值的分支

颜色无需额外操作:一旦 <meta> 映射到 color-scheme(如上所示),light-dark() 就会遵循替换。 本部分介绍如何调整其他值(例如 font-weight、媒体来源等)。

如果网页只是遵循系统偏好设置,则可以使用 (prefers-color-scheme: dark) 进行分支,包括在 <picture> 或 <video> 来源中。替换配色方案会增加额外的复杂性:系统偏好设置不再是可靠来源,对于仅接受媒体查询的 API,没有直接的替代方案。

请勿完全依赖于 JS 应用的类(例如 .dark)进行分支,因为如果 JS 未加载且操作系统默认设置为深色,这些类将不正确。

您可以将媒体查询与 :root:has(> head > meta[name="color-scheme"][content="dark"])(或 light)选择器结合使用,以创建分支。 如需详细了解如何巧妙地实现这一点,请参阅选择器和 @ 规则组合。

控件本身的选项也是非颜色分支:在标记中包含两种状态(图标以及视觉上隐藏的操作文本,例如“切换到深色主题”),并通过这些选择器隐藏非活动状态。 display: none 还会从 AT 中移除多余的选项,因此当系统偏好设置发生变化时,控件无需 JS 即可保持最新状态。

对于 HTML 内嵌媒体,目前唯一的办法是将两个版本作为单独的元素包含在内,并适当地切换可见性。 使用 display: none 隐藏 <source> 元素不起作用。

界面

两种还是三种状态?

在所有情况下,界面都必须支持将配色方案还原或设置为系统配色方案。

在空间受限的切换开关中,最好显示两种状态,例如网站 Chrome(标题、导航、页脚等)中的常驻切换开关,这些位置空间有限,且调整设置通常与用户目标无关。

在单独的设置界面(页面、面板或叠加层)中,配色方案控件属于不同的场景,可以明确显示所有三种状态(“浅色”“深色”“系统”):在这种情况下,用户已经对未来的行为做出了深思熟虑的决定,并且有空间来解释这些选项。

如果网站实现了情境感知方案(例如,当操作系统处于深色模式时,网站会采用较暗的浅色模式),那么也需要提供三种明确的状态,因为此时“浅色”和“系统(当前为浅色)”确实存在差异。

双态切换开关

使用此类控件时,往往是为了临时调整舒适度(“现在太亮了”),而不是为了确保当前状态长期保持不变。 因此,对于双态切换开关,只需显示三种设置中的两种即可:系统设置(以便还原设置)和当前与系统设置相反的设置(存储为其当前字面量值)。

仅有的两个状态应为:

  1. 系统默认 - 无存储值,<meta> 内容 light dark。显示为其当前解析的值(例如,浅色主题时显示太阳图标)。
  2. 替换 - 按字面存储为 light 或 dark。

本质上,它是一个三态控件(light dark、light、dark),其中与当前系统偏好设置匹配的显式状态无法访问。

当用户切换以下设置时:

  1. 目标配色方案 = 与当前呈现的配色方案相反(存储的值 [如果有],否则为系统偏好设置)。用户意图是“选择与我当前看到的相反的选项”,而不是“选择与系统默认设置相反的选项”。
  2. 如果目标语言与当前系统偏好设置不同,请按字面意思存储。
  3. 如果目标值与当前系统偏好设置一致,则表示用户正在撤消其调整:移除存储的值。请勿存储该值,否则会隐式固定该方案,使其无法适应未来的系统更改。
  4. 将 <meta> 内容设置为存储的值,如果没有,则设置为 light dark。

重要提示:必须仅在存储时检查差异,绝不能追溯检查:如果系统偏好设置后来更改为与存储的值匹配,则必须保留该存储的值。 许多用户的操作系统会根据一天中的时间自动切换配色方案;如果每当两者恰好一致时就移除存储的值,则根本无法固定配色方案。

示例场景,从操作系统设置为浅色开始:

  1. 用户切换。深色与系统偏好设置不同,因此系统会存储 dark;网站会变为深色。
  2. 操作系统设置会更改为深色。网站保持深色模式(存储的值现在与系统偏好设置一致,但仍会保留)。
  3. 操作系统设置会改回浅色。网站保持深色模式。
  4. 用户切换。目标(浅色)与系统偏好设置一致,因此存储的值会被移除,网站会再次遵循系统设置。

三态切换开关

当有更多空间,且用户已经有设置长期偏好设置的想法时,三态控件可以提供更清晰的信息。 在这种情况下,数据模型会清晰地映射到三个界面选项:系统、浅色和深色。

为了减轻认知负荷,“系统”选项还应显示当前的系统设置,例如:

  • 图标,例如系统图标右下角的太阳图标
  • 文本,例如“系统(当前为浅色)”