Preferreds-color-scheme:你好,黑暗,我的老朋友

Overhyped or necessity? 全面了解深色模式以及如何支持该模式,让您的用户受益!

简介

深色模式之前的深色模式

一台 90 年代前的旧电脑,黑色背景上显示着绿色文字。
绿幕(来源)

深色模式的发展历程兜兜转转,又回到了原点。 在个人计算时代初期,深色模式并非一种选择,而是事实:单色 CRT 计算机显示器通过在磷光屏幕上发射电子束来工作,而早期 CRT 中使用的磷光体是绿色的。由于文本以绿色显示,而屏幕的其余部分为黑色,因此这些模型通常被称为绿屏。

显示器:屏幕上显示的是 Windows 3.1 版本的 Microsoft Word。
白底深色(来源)

随后推出的彩色 CRT 通过使用红色、绿色和蓝色荧光粉来显示多种颜色。 他们通过同时激活所有三种荧光粉来创造白色。 随着更复杂的WYSIWYG桌面出版的出现,让虚拟文档类似于实体纸张的想法开始流行。

一组窗口,显示早期 WorldWideWeb 浏览器中的白底黑字网页。
WorldWideWeb 浏览器(来源)

这就是白底黑字设计趋势的起源,这种趋势一直延续到早期的文档型网络。有史以来的第一个浏览器 WorldWideWeb(请记住,当时甚至还没有发明 CSS)就是这样显示网页的。有趣的是,有史以来的第二个浏览器 Line Mode Browser(一种基于终端的浏览器)是深色背景上的绿色。如今,网页和 Web 应用通常采用浅色背景深色文本的设计,这也是用户代理样式表(包括 Chrome 的样式表)中硬编码的基本假设。

在床上使用手机

CRT 时代早已一去不复返。 内容消费和创作已转移到使用背光 LCD 或节能 AMOLED 屏幕的移动设备上。更小、更便携的计算机、平板电脑和智能手机催生了新的使用模式。 浏览网页、趣味编程和高端游戏等休闲活动通常发生在下班后的昏暗环境中。 人们甚至喜欢在晚上躺在床上使用设备。 在黑暗中使用设备的人越多,深色背景浅色文字这一回归本源的想法就越受欢迎。

为何使用深色模式

出于美观原因而采用深色模式

当人们被问及为什么喜欢或想要深色模式时,最常见的回答是“它更护眼”,其次是“它优雅美观”。 Apple 在其深色模式开发者文档中明确写道:“对于大多数用户来说,选择启用浅色外观还是深色外观是一种审美选择,可能与环境光照条件无关。”

Mac OS System 7 中采用“黑底白字”模式的 CloseView(来源)

深色模式作为无障碍工具

还有一些人确实需要深色模式,并将其用作另一种无障碍工具,例如低视力用户。 我能找到的最早的此类无障碍工具是 System 7 的 CloseView 功能,该功能有一个黑底白字和白底黑字切换开关。 虽然 System 7 支持彩色,但默认界面仍是黑白的。

这些基于反转的实现在引入颜色后暴露出了其弱点。 Szpiro 等人针对低视力用户如何使用计算设备开展的用户调研表明,所有受访用户都不喜欢反色图片,但许多用户更喜欢深色背景上的浅色文字。 Apple 通过一项名为智能反转的功能来满足用户的这一偏好,该功能会反转屏幕上的颜色,但图片、媒体和一些使用深色样式的应用除外。

低视力的一种特殊形式是计算机视觉综合症,也称为数字眼疲劳,定义为“与使用计算机(包括台式机、笔记本电脑和平板电脑)和其他电子显示器(例如智能手机和电子阅读设备)相关的眼部和视力问题。” 有研究表明,青少年使用电子设备(尤其是在夜间)会导致睡眠时长缩短、入睡延迟时间延长以及睡眠不足的风险增加。 此外,根据 Rosenfield 的研究,广泛报道表明,接触蓝光会影响昼夜节律和睡眠周期的调节,而不规律的光照环境可能会导致睡眠不足,从而可能影响情绪和任务表现。 为了限制这些负面影响,您可以通过 iOS 的 Night Shift 或 Android 的夜间光效等功能调整显示色温,从而减少蓝光,还可以通过深色主题或深色模式避免强光或不规则光。

AMOLED 屏幕上的深色模式节能效果

最后,众所周知,深色模式在 AMOLED 屏幕上可以节省大量电量。针对 YouTube 等热门 Google 应用的 Android 案例研究表明,省电幅度最高可达 60%。以下视频详细介绍了这些案例研究以及每个应用的节能效果。

在操作系统中启用深色模式

现在,我已经介绍了为什么深色模式对许多用户来说如此重要,接下来我们来了解一下如何支持深色模式。

Android Q 深色模式设置

支持深色模式或深色主题的操作系统通常会在设置中的某个位置提供启用该模式或主题的选项。 在 macOS X 上,它位于系统偏好设置的常规部分,名为外观(屏幕截图);在 Windows 10 上,它位于颜色部分,名为选择颜色(屏幕截图)。 对于 Android Q,您可以在显示下找到深色主题切换开关(屏幕截图);对于 iOS 13,您可以在设置的显示与亮度部分中更改外观(屏幕截图)。

prefers-color-scheme 媒体查询

在开始之前,我还要再讲一点理论。 媒体查询允许作者测试和查询用户代理或显示设备的值或功能,而无需考虑正在呈现的文档。 它们用于 CSS @media 规则中,以便有条件地将样式应用于文档,以及各种其他上下文和语言(例如 HTML 和 JavaScript)。媒体查询级别 5 引入了所谓的用户偏好媒体功能,即网站检测用户首选内容显示方式的一种方式。

prefers-color-scheme 媒体功能用于检测用户是否已请求网页使用浅色或深色主题。它适用于以下值:

  • light:表示用户已通知系统,他们更喜欢采用浅色主题(浅色背景上使用深色文本)的页面。
  • dark:表示用户已通知系统,他们更喜欢采用深色主题(深色背景上显示浅色文本)的网页。

支持深色模式

了解浏览器是否支持深色模式

由于深色模式是通过媒体查询报告的,因此您只需检查媒体查询 prefers-color-scheme 是否匹配,即可轻松检查当前浏览器是否支持深色模式。 请注意,我没有添加任何值,只是检查媒体查询本身是否匹配。

if (window.matchMedia('(prefers-color-scheme)').media !== 'not all') {
  console.log('🎉 Dark mode is supported');
}

在撰写本文时,Chrome 和 Edge(从版本 76 开始)、Firefox(从版本 67 开始)以及 Safari(从 macOS 上的版本 12.1 开始,以及 iOS 上的版本 13 开始)在桌面设备和移动设备(如果可用)上均支持 prefers-color-scheme。 对于所有其他浏览器,您可以查看 Can I use 支持表文档。

在请求时了解用户偏好

Sec-CH-Prefers-Color-Scheme 客户端提示标头允许网站在请求时选择性地获取用户的配色方案偏好设置,从而使服务器能够内嵌正确的 CSS,避免出现错误的色彩主题。

深色模式实际效果

最后,我们来看看支持深色模式的实际效果。 就像高地人一样,深色模式只能有一个:深色或浅色,但绝不能两者兼有!我为什么要提到这一点?因为这一事实应该会对加载策略产生影响。 请勿强制用户在关键渲染路径中下载适用于其当前未使用模式的 CSS。 因此,为了优化加载速度,我将示例应用的 CSS 拆分为三个部分,以便推迟非关键 CSS,该应用在实践中显示了以下建议:

  • style.css,其中包含在网站上普遍使用的通用规则。
  • 仅包含深色模式所需规则的 dark.css。
  • light.css,其中仅包含浅色模式所需的规则。

加载策略

后两个(light.css 和 dark.css)会通过 <link media> 查询有条件地加载。 最初,并非所有浏览器都支持 prefers-color-scheme(可使用上述模式检测到),我通过在微型内嵌脚本中通过有条件插入的 <link rel="stylesheet"> 元素加载默认 light.css 文件来动态处理此问题(浅色是任意选择,我也可以将深色作为默认后备体验)。 为了避免无样式内容闪烁,我会隐藏网页内容,直到 light.css 加载完毕。

<script>
  // If `prefers-color-scheme` is not supported, fall back to light mode.
  // In this case, light.css will be downloaded with `highest` priority.
  if (window.matchMedia('(prefers-color-scheme: dark)').media === 'not all') {
    document.documentElement.style.display = 'none';
    document.head.insertAdjacentHTML(
      'beforeend',
      '<link rel="stylesheet" href="/light.css" onload="document.documentElement.style.display = \'\'">',
    );
  }
</script>
<!--
  Conditionally either load the light or the dark stylesheet. The matching file
  will be downloaded with `highest`, the non-matching file with `lowest`
  priority. If the browser doesn't support `prefers-color-scheme`, the media
  query is unknown and the files are downloaded with `lowest` priority (but
  above I already force `highest` priority for my default light experience).
-->
<link rel="stylesheet" href="/dark.css" media="(prefers-color-scheme: dark)" />
<link
  rel="stylesheet"
  href="/light.css"
  media="(prefers-color-scheme: light)"
/>
<!-- The main stylesheet -->
<link rel="stylesheet" href="/style.css" />

样式表架构

我充分利用了 CSS 变量,这使得我的通用 style.css 能够保持通用性,而所有浅色或深色模式的自定义设置都在另外两个文件 dark.css 和 light.css 中进行。 下面是实际样式的摘录,但应该足以传达整体思路。 我声明了两个变量:-⁠-⁠color 和 -⁠-⁠background-color,它们基本上创建了浅色背景深色文字和深色背景浅色文字的基准主题。

/* light.css: 👉 dark-on-light */
:root {
  --color: rgb(5, 5, 5);
  --background-color: rgb(250, 250, 250);
}
/* dark.css: 👉 light-on-dark */
:root {
  --color: rgb(250, 250, 250);
  --background-color: rgb(5, 5, 5);
}

然后,在我的 style.css 中,我在 body { … } 规则中使用这些变量。 由于它们是在 :root CSS 伪类(在 HTML 中表示 <html> 元素的选择器,与选择器 html 相同,但其特异性更高)上定义的,因此它们会级联,这有助于我声明全局 CSS 变量。

/* style.css */
:root {
  color-scheme: light dark;
}

body {
  color: var(--color);
  background-color: var(--background-color);
}

在上面的代码示例中,您可能已经注意到一个属性 color-scheme,其值为以空格分隔的 light dark。

这会告知浏览器我的应用支持哪些色彩主题,并允许浏览器激活用户代理样式表的特殊变体,这有助于让浏览器呈现具有深色背景和浅色文本的表单字段、调整滚动条,或启用可感知主题的高亮颜色。 CSS 颜色调整模块级别 1 中指定了 color-scheme 的确切细节。

其他一切都只是为网站上的重要内容定义 CSS 变量。 在处理深色模式时,对样式进行语义化整理会很有帮助。 例如,请考虑将变量命名为 -⁠-⁠accent-color,而不是 -⁠-⁠highlight-yellow,因为“yellow”在深色模式下可能实际上不是黄色,反之亦然。以下是我在示例中使用的其他一些变量。

/* dark.css */
:root {
  --color: rgb(250, 250, 250);
  --background-color: rgb(5, 5, 5);
  --link-color: rgb(0, 188, 212);
  --main-headline-color: rgb(233, 30, 99);
  --accent-background-color: rgb(0, 188, 212);
  --accent-color: rgb(5, 5, 5);
}
/* light.css */
:root {
  --color: rgb(5, 5, 5);
  --background-color: rgb(250, 250, 250);
  --link-color: rgb(0, 0, 238);
  --main-headline-color: rgb(0, 0, 192);
  --accent-background-color: rgb(0, 0, 238);
  --accent-color: rgb(250, 250, 250);
}

完整示例

在以下嵌入式演示中,您可以看到一个已完成示例,其中将上述概念付诸实践。尝试在特定操作系统的设置中切换深色模式,看看页面有何反应。

正在加载影响

在试用此示例时,您可以看到我为何通过媒体查询加载 dark.css 和 light.css。 尝试切换深色模式并重新加载页面:当前不匹配的特定样式表仍会加载,但优先级最低,因此它们绝不会与网站此时此刻所需的资源竞争。

网络加载图,展示了在浅色模式下,深色模式 CSS 如何以最低优先级加载。
浅色模式下的网站会以最低优先级加载深色模式 CSS。
网络加载图,展示了在深色模式下,浅色模式 CSS 如何以最低优先级加载。
深色模式下的网站会以最低优先级加载浅色模式 CSS。
网络加载图,展示了在默认浅色模式下,深色模式 CSS 如何以最低优先级加载。
在不支持 prefers-color-scheme 的浏览器上,默认采用浅色模式的网站会以最低优先级加载深色模式 CSS。

响应深色模式更改

与任何其他媒体查询更改一样,您可以通过 JavaScript 订阅深色模式更改。 例如,您可以使用此功能动态更改网页的网站图标,或更改用于确定 Chrome 中网址栏颜色的 <meta name="theme-color">。上面的完整示例展示了实际效果,如需查看主题颜色和网站图标的变化,请在单独的标签页中打开演示。

const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
darkModeMediaQuery.addEventListener('change', (e) => {
  const darkModeOn = e.matches;
  console.log(`Dark mode is ${darkModeOn ? '🌒 on' : '☀️ off'}.`);
});

从 Chromium 93 和 Safari 15 开始,您可以使用 meta 主题颜色元素的 media 属性,根据媒体查询调整颜色。系统会选择第一个匹配的。例如,您可以为浅色模式设置一种颜色,为深色模式设置另一种颜色。在撰写本文时,您无法在清单中定义这些内容。请参阅 w3c/manifest#975 GitHub 问题。

<meta
  name="theme-color"
  media="(prefers-color-scheme: light)"
  content="white"
/>
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="black" />

调试和测试深色模式

在开发者工具中模拟 prefers-color-scheme

切换整个操作系统的配色方案可能会很快让人感到厌烦,因此 Chrome 开发者工具现在允许您以仅影响当前可见标签页的方式模拟用户的首选配色方案。 打开命令菜单,开始输入 Rendering,运行 Show Rendering 命令,然后更改模拟 CSS 媒体功能 prefers-color-scheme 选项。

Chrome 开发者工具的“渲染”标签页中“模拟 CSS 媒体功能 prefers-color-scheme”选项的屏幕截图。

使用 Puppeteer 对 prefers-color-scheme 进行截屏

Puppeteer 是一个 Node.js 库,提供了一个高级 API,用于通过 DevTools 协议控制 Chrome 或 Chromium。借助 dark-mode-screenshot,我们提供了一个 Puppeteer 脚本,可让您在深色和浅色模式下创建网页的屏幕截图。您可以一次性运行此脚本,也可以将其作为持续集成 (CI) 测试套件的一部分。

npx dark-mode-screenshot --url https://googlechromelabs.github.io/dark-mode-toggle/demo/ --output screenshot --fullPage --pause 750

深色模式最佳实践

避免使用纯白色

您可能已经注意到一个细节,那就是我没有使用纯白色。 相反,为了防止与周围的深色内容产生发光和溢色效果,我选择了一种略深的白色。rgb(250, 250, 250) 这样的名称就很合适。

重新着色并调暗摄影图像

如果您比较以下两个屏幕截图,会发现不仅核心主题从浅色背景深色文字更改为深色背景浅色文字,而且主打图片看起来也略有不同。 我的用户调研表明,大多数受访者更喜欢在深色模式下使用稍微不那么鲜艳和明亮的图片。 我将其称为重新着色。

在深色模式下,主打图片略微变暗。
浅色模式下的常规主打图片。

您可以通过对图片应用 CSS 滤镜来实现重新着色。 我使用了一个 CSS 选择器,它会匹配网址中不包含 .svg 的所有图片,这样我就可以对矢量图形(图标)和图片(照片)采用不同的重新着色处理,下一段对此进行了详细介绍。 请注意,我再次使用了 CSS 变量,以便日后灵活更改滤镜。

由于仅在深色模式下(即 dark.css 处于活动状态时)才需要重新着色,因此 light.css 中没有相应的规则。

/* dark.css */
--image-filter: grayscale(50%);

img:not([src*='.svg']) {
  filter: var(--image-filter);
}

使用 JavaScript 自定义深色模式重新着色强度

每个人的情况都不一样,对深色模式的需求也不同。 通过坚持使用上述重新着色方法,我可以轻松地将灰度强度设置为用户偏好设置,以便通过 JavaScript 进行更改,并且通过设置 0% 值,我还可以完全停用重新着色。 请注意,document.documentElement 提供了对文档根元素的引用,即我可以使用 :root CSS 伪类引用的同一元素。

const filter = 'grayscale(70%)';
document.documentElement.style.setProperty('--image-filter', value);

反转矢量图形和图标

对于矢量图形(在我的例子中,它们用作我通过 <img> 元素引用的图标),我使用不同的重新着色方法。 虽然研究表明,人们不喜欢照片反色,但反色对大多数图标来说效果非常好。同样,我使用 CSS 变量来确定常规状态和 :hover 状态下的反转量。

黑色背景,白色图标和文字。
图标在深色模式下会反转。
白底黑字和黑色图标。
浅色模式下的常规图标。

请注意,我同样只在 dark.css 中反转图标,而不在 light.css 中反转,并且 :hover 在这两种情况下会获得不同的反转强度,使图标看起来略暗或略亮,具体取决于用户选择的模式。

/* dark.css */
--icon-filter: invert(100%);
--icon-filter_hover: invert(40%);

img[src*='.svg'] {
  filter: var(--icon-filter);
}
/* light.css */
--icon-filter_hover: invert(60%);
/* style.css */
img[src*='.svg']:hover {
  filter: var(--icon-filter_hover);
}

对内嵌 SVG 使用 currentColor

对于内嵌 SVG 图片,您可以利用表示元素 color 属性值的 currentColor CSS 关键字,而不是使用反转滤镜。这样,您就可以在默认情况下不接收 color 值的属性上使用该值。 方便的是,如果将 currentColor 用作 SVG fill 或 stroke 属性的值,它会改为从 color 属性的继承值中获取其值。更棒的是,这也适用于 <svg><use href="…"></svg>,因此您可以拥有单独的资源,并且 currentColor 仍会在上下文中应用。请注意,这仅适用于内嵌或 <use href="…"> SVG,不适用于作为图片的 src 或通过 CSS 引用的 SVG。您可以在下面的演示中看到此应用。

<!-- Some inline SVG -->
<svg xmlns="http://www.w3.org/2000/svg"
    stroke="currentColor"
>
  […]
</svg>

在不同模式之间顺畅切换

从深色模式切换到浅色模式(反之亦然)可以变得更加平滑,因为 color 和 background-color 都是可动画化的 CSS 属性。 创建动画就像为这两个属性声明两个 transition 一样简单。 以下示例说明了整体思路,您可以在演示中进行体验。

body {
  --duration: 0.5s;
  --timing: ease;

  color: var(--color);
  background-color: var(--background-color);

  transition: color var(--duration) var(--timing), background-color var(
        --duration
      ) var(--timing);
}

深色模式下的艺术指导

虽然出于加载性能方面的考虑,我通常建议仅在 <link> 元素的 media 属性中使用 prefers-color-scheme(而不是在样式表中内嵌),但在某些情况下,您可能确实希望直接在 HTML 代码中内嵌 prefers-color-scheme。 艺术指导就是这样一种情况。 在网页设计中,艺术指导负责页面的整体视觉外观,以及如何通过视觉方式进行沟通、激发情绪、对比特征,并在心理上吸引目标受众。

在深色模式下,设计师需要自行判断在特定模式下哪种图片效果最好,以及重新着色是否可能不够好。 如果与 <picture> 元素搭配使用,则要显示的图片的 <source> 可以取决于 media 属性。 在下面的示例中,我为深色模式显示西半球,为浅色模式或未指定偏好设置时显示东半球,在所有其他情况下默认显示东半球。 当然,这纯粹是为了说明问题。在设备上切换深色模式,看看有何不同。

<picture>
  <source srcset="western.webp" media="(prefers-color-scheme: dark)" />
  <source srcset="eastern.webp" media="(prefers-color-scheme: light)" />
  <img src="eastern.webp" />
</picture>

深色模式,但添加选择退出选项

如上文为何选择深色模式部分所述,深色模式是大多数用户的一种审美选择。因此,有些用户可能确实希望其操作系统界面采用深色,但仍希望以习惯的方式查看网页。 一种很好的模式是,最初遵循浏览器通过 prefers-color-scheme 发送的信号,但随后可以选择允许用户替换其系统级设置。

<dark-mode-toggle> 自定义元素

当然,您可以自行编写代码,也可以直接使用我为此目的创建的现成自定义元素(Web 组件)。 它名为 <dark-mode-toggle>,可向您的页面添加一个切换开关(深色模式:开启/关闭)或主题切换器(主题:浅色/深色),您可以对其进行完全自定义。 下面的演示展示了该元素的实际效果(哦,我还🤫悄悄地将其应用到了其他所有示例中)。

<dark-mode-toggle
  legend="Theme Switcher"
  appearance="switch"
  dark="Dark"
  light="Light"
  remember="Remember this"
></dark-mode-toggle>
浅色模式下的 <dark-mode-toggle>。
深色模式下的 <dark-mode-toggle>。

请尝试点击或点按以下演示中右上角的深色模式控件。 如果您选中第三个和第四个控件中的复选框,请观察即使您重新加载页面,系统也会记住您的模式选择。 这样,访问者就可以让操作系统保持深色模式,但以浅色模式浏览您的网站,反之亦然。

总结

使用和支持深色模式很有趣,并且开辟了新的设计途径。 对于某些访问者来说,这可能意味着他们无法使用您的网站,也可能意味着他们成为满意的用户。 虽然存在一些陷阱,并且绝对需要进行仔细测试,但深色模式绝对是一个绝佳的机会,可以表明您关心所有用户。 本文中提到的最佳实践以及 <dark-mode-toggle> 自定义元素等辅助工具应该会让您有信心打造出色的深色模式体验。请在 Twitter 上告诉我您创建的内容、本文是否对您有用,以及您对本文的改进建议。 感谢阅读!🌒

prefers-color-scheme 媒体查询的资源:

color-scheme 元标记和 CSS 属性的相关资源:

深色模式常规链接:

此帖子的背景研究文章:

致谢

prefers-color-scheme 媒体功能、color-scheme CSS 属性和相关元标记是 👏 Rune Lillesveen 的实现工作。 Rune 还是 CSS 颜色调整模块第 1 级规范的联合编辑。 在此,我要 🙏 感谢 Lukasz Zbylut、Rowan Merewood、Chirag Desai 和 Rob Dodson 对本文的全面审核。 加载策略是 Jake Archibald 的创意。Emilio Cobos Álvarez 向我指出了正确的 prefers-color-scheme 检测方法。 有关引用 SVG 和 currentColor 的提示来自 Timothy Hatcher。 最后,我要感谢参与各种用户调研的众多匿名参与者,他们帮助我们形成了本文中的建议。