原文链接:https://blog.skk.moe/post/hello-darkmode-my-old-friend/
原文作者:Sukka
前几天为我的 Hexo 主题:Miracle 加入了深色模式,但我的技术还是太辣鸡,经常出现问题。
无意间看到 Sukka 大佬的文章:「你好黑暗,我的老朋友 —— 为网站添加用户友好的深色模式支持」,跟着文章重构了主题深色模式的代码,就转载过来方便学习。
什么是「深色模式」
很多操作系统在日落后会自动切换到「深色模式」、并不意味着「深色模式」就是「夜间模式」。「夜间模式」用于夜晚的弱光环境,主要目的是保护眼睛、减少强光刺激、避免影响睡眠,不难理解为什么 macOS 的 Night Shift 会自动调节屏幕色温、Android(AOSP)到了夜间可以选择启用系统级「琥珀色」滤镜。
「深色模式」更像是一个主题,即使在白天也可以使用。不论是为了在 OLED 屏幕上省电、亦或是减少白光刺激护眼、亦或是暗色模式对色盲用户更加友好,总之 macOS 率先提出了系统级的「暗色模式」、并在 WebKit 中增加了对应的 Media Query,而后 Chromium、Firefox 先后跟进,如今兼容 prefers-color-scheme
的浏览器占有率已经高达 81.82%。
利用 Media Query 简单实现深色模式
CSS 媒体查询 @media
是一个足够强大的特性,可以有条件地将样式应用于文档和各种上下文中。Media Queries Level 5 草案 中提出了深色模式的判断方式 prefers-color-scheme
,包含 light
、dark
、no-preference
三种值。而不支持 Media Queries 5 的浏览器会直接无视 CSS 中的 prefers-color-scheme
Media Query,无需额外的代码即可优雅降级。
还记得我刚刚说过「深色模式更像一个主题」么?为网站新增深色模式就如同换肤功能;搭配 prefers-color-scheme
,编写深色模式的思路就如同编写响应式一般、无需赘述,结合几段 Code Snippet 一笔带过:
CSS Variable 的方法实现深色模式
:root { |
通过维护两套 CSS Variable,可以快速切换不同的配色方案。这种方法特点是所需代码较少,缺点是 CSS Variable 的兼容性较差,可能还需要引入额外的 Polyfill。
为深色模式单独编写样式
body { |
直接维护两套样式的方法清晰直观、任何网站都可以基于这种方法进行改造。但会造成冗余代码、较难实现统一的风格、后期不易维护。
条件性加载深色模式的 CSS 文件
/* main.css */ |
<link rel="stylesheet" href="main.css"> |
利用 <link>
标签的 Media Query,甚至可以单独加载暗色模式的 CSS 文件。
需要注意 CSS 选择器的权重,因此作为可选的
dark.css
一定要放在main.css
之后加载。
除了上述三种方式以外,使用 CSS filter
或 mix-blend-mode
还可以实现对网站整体色调的改变,可以确保配色风格的统一性。
「深色模式」的兼容性
虽然有了优雅的 prefers-color-scheme
可以识别操作系统的显示模式,但是对于用户来说,仅依赖 Media Query 的「深色模式」并不能带来很好的体验。
首先是浏览器兼容性。虽然支持该特性的浏览器的市场占有率非常喜人,但是从版本号上来看却并不乐观:
考虑到使用 Chormium 70 内核甚至 Tencent X5 内核的国产浏览器,大部分用户并没有机会体验到深色模式。除此以外,操作系统级别的「深色模式」实现也会受到 OEM 厂商的影响 —— 虽然 Android 10(AOSP)提供「深色模式」,但是一加的 OxygenOS 却将其深藏在系统主题设置里,没有自动切换、在 Quick Settings 里也没有快速的切换开关。
设计一个用户友好的「深色模式」
受限于兼容性和复杂的操作系统,大部分网站依然在使用更传统的「开关」切换 —— 通过 toggle <html>
或<body>
的 class 属性实现在两套样式之间切换、并将开关的状态记忆在 localStorage 中的方法虽然有效,却是无奈之举,手动切换开关相比 prefers-color-scheme
也不够优雅。如果将「开关」和 prefers-color-scheme
结合起来,就可以带来更好的用户体验:
- 对于不兼容的浏览器或操作系统,访客依然可以通过开关手动切换显示模式
- 对于兼容的浏览器或操作系统,Media Query 能够实现在两种显示模式之间切换
- 在兼容的浏览器或操作系统上,用户还可以通过开关 override 当前的显示模式
在将两者组合在一起时,不能简单地用「开关」覆盖 prefers-color-scheme
,否则用户触发开关、状态被永久记忆在 localStorage 之后,就变成了僵硬的手动模式。
举个例子。访客可能在操作系统还没有自动切换到「深色模式」时通过网站上的开关切换显示模式,经过一个夜晚后到了次日白天、访客再度访问网站时,自然希望不需要再切换开关、网站就能以常规的浅色模式显示。因此设计思路是当 prefers-color-scheme
的值发生改变(从 与用户需要的显示模式不同 变成 相同)时清空 localStorage 中储存的开关状态,此时显示模式切换回基于 Media Query 的「自动」模式。
Talk is cheap, here goes the code.
首先是 CSS:
:root { |
真是令人看的头大,让我们逐行来看都是些什么:
- 在
:root
下定义了一个 CSS Variable--color-mode: light
和在浅色模式下用到的 CSS Variable(比如使用深色#333
作为主要字体颜色)。 - 使用
prefers-color-scheme
的 Media Query 定义深色模式下的 CSS Variable:--color-mode: light
。深色模式的样式(如浅色#eff
作为主要字体颜色)要定义在:not([data-user-color-scheme])
伪类下以避免「开关」的行为覆盖浏览器的样式。 - 为
[data-user-color-scheme='dark']
再定义一遍深色模式下用到的样式。
有了这段 CSS,不难理解深色模式何时会生效:当操作系统使用「深色模式」且<html>
或<body>
标签上没有data-user-color-scheme
属性时、或者存在data-user-color-scheme
属性且值为dark
时。
然后是困难的部分了:编写 JavaScript 为「开关」添加行为。
先定义一些常量:
const rootElement = document.documentElement; // <html> |
接下来,用 try {} catch (e) {}
封装一下 localStorage 的操作,以应对 HTML5 Storage 被禁用、localStorage 被写满、localStorage 实现不完整的情况:
const setLS = (k, v) => { |
我们还需要一个函数读取当前 prefers-color-scheme
的方法。由于已经在 CSS 中定义了 --color-mode
,所以在 JS 中直接读取就好了:
const getModeFromCSSMediaQuery = () => { |
还记得我们需要自动取消手动模式回到 prefers-color-scheme
么?意味着我们需要一个函数清掉 LS、删掉 <html>
存在的 data-user-color-scheme
属性:
const resetRootDarkModeAttributeAndLS = () => { |
接下来是起主要作用的函数了,负责为 <html>
标签修改 data-user-color-scheme
属性:
const validColorModeKeys = { |
当然,「开关」还需要一个函数,这个函数负责获取相反的显示模式,同时还要将新的模式写入 localStorage 存储起来:
const invertDarkModeObj = { |
相关的函数都定义完了,是时候添加函数执行了:
// 当页面加载时,将显示模式设置为 localStorage 中自定义的值(如果有的话) |
我的博客也使用的这种实现,通过 Navbar 中的按钮体验一下吧!