给博客添加 dark mode
在整理周刊的时候,看到 Come to the light-dark() Side,一篇关于如何设置 dark mode 的很棒的文章。
我个人比较喜欢 light mode,除了代码编辑器外,其他都是 light mode。
我觉得 light mode 看起来比较舒服和自然,dark mode 下黑底白字比较刺眼。
但是文章里说到,不同人群有不同的需要:
- 飞蚊症患者可能更喜欢 dark mode(可能是 dark mode 下飞蚊不那么明显?)
- 散光患者可能在 light mode 下更容易聚焦
不管如何,或许有人需要 dark mode,我也想实践一下文章中的方法,所以我给博客加上了 dark mode 啦ヾ(´∀ ˋ)ノ
本来以为是一件不怎么麻烦的事情,但实践下来要处理的内容还不少,接下来我会分享是如何实现的,以及碰到的问题,包括:
- 设置 dark mode
- 闪烁问题
- dark mode 下 giscus 的处理
- dark mode 下 iframe 的处理
- dark mode 下代码块高亮的处理
设置 dark mode
1. 在 <head>
中添加 <meta name="color-scheme" content="light dark">
(@see: init-org-publish.el)
适配 dark mode 的一种方法是设置 color-scheme,可以在 <meta>
中添加,也可以在元素上设置,例如:
<head> <!-- 其他内容 --> <meta name="color-scheme" content="light dark"> <!-- 其他内容 --> </head> <!-- 也可以在元素上设置 --> <div style="color-scheme: light dark;">...</div>
:root { color-scheme: light dark; } main { color-scheme: light dark; }
设置了 color-scheme 之后,浏览器就会自动适配页面的颜色,例如设置 color-scheme: dark;
,那么页面就会变成 dark mode, 整体偏黑色。
但是浏览器只会改变那些没有主动设置颜色的元素,如果你给一个元素设置了 color
, background-color
等,浏览器是不知道如何切换颜色的。
所以,接下来需要用 CSS,告诉浏览器在 color-scheme: dark;
下用什么颜色。
2. CSS 中通过 light-dark() 设置不同的颜色
按照 color-scheme 文档中的介绍,有两种办法去适配颜色:
-
:root { color-scheme: light dark; } @media (prefers-color-scheme: light) { .element { color: black; background-color: white; } } @media (prefers-color-scheme: dark) { .element { color: white; background-color: black; } }
使用 light-dark() 这是 Come to the light-dark() Side 中推荐的方法。
:root { color-scheme: light dark; } .element { /* fallback 的颜色,当用户浏览器不支持 color: light-dark(black, white); 时,回退到这个颜色 */ color: black; /* light mode 下 color 用 black, dark mode 下 color 用 white */ color: light-dark(black, white); background-color: white; background-color: light-dark(white, black); }
博客中,我主要是用 light-dark() 去设置不同的颜色,部分地方使用 prefers-color-scheme,具体可以看博客的 style.css。
需要注意的是,浏览器可能不支持 light-dark()
,需要设置一个回退的样式,例如:
.element { color: black; color: light-dark(black, white); }
关于兼容性问题,推荐看看 A Framework for Evaluating Browser Support,里面介绍了在使用较新的 CSS 特性的时候需要考虑什么,以及如何测试。
3. 页面上通过 <select>
去切换 <meta name="color-scheme">
的值,记住读者选择的 color-scheme
经过上面 1,2 两步,现在已经拥有一个基于 color-scheme 的博客了。
如果你设置的是 color-scheme: light dark;
,浏览器会基于系统的设置,自动切换 light mode 和 dark mode。
但是可能有的读者喜欢 light mode,有的读者喜欢 dark mode,希望能固定 color-scheme ,所以还需要在页面提供一个切换 color-scheme 的地方。
方法有很多,我是在博客的导航栏右侧放置了一个 <select>
,然后写了一点 JavaScript 去控制,具体见 color-scheme.js。
除了开放切换 color-scheme 外,还需要记住读者的 color-scheme 配置,这样读者下次进来就还是他之前的配置,相对友好一些,目前我是存储在 localStorage 中。
这样就拥有一个可以切换 light ,dark 的博客啦 。:.゚ヽ(*´∀`)ノ゚.:。
但事情并没有这么简单,过程中我还碰到了不少问题,接下来逐个看看。
踩坑
闪烁问题
最开始 color-scheme.js 是放在 HTML 的最后加载的,避免影响页面内容的加载和渲染。
但因为 <meta name="color-scheme">
的默认值是 light dark
,假如系统配置中是 light mode ,而读者之前选择了 dark mode,页面加载的时候就会出现从 light mode 到 dark mode 的一个短暂转换,看起来像闪烁了一下。
为了避免这个问题,我将 color-scheme.js 挪到了 HTML 的开头部分,一开始就按照读者之前选择的值,设置 color-scheme,这样在内容渲染之前,color-scheme 就已经切换好了,避免闪烁。
但这样又引入了另一个问题,由于 color-scheme.js 执行的时候,页面的 DOM 还没渲染,因此无法提前获取 DOM,设置 <select>
的选中值,以及其他 DOM 元素的样式。
所以 color-scheme.js 中还需要监听 DOMContentLoaded 事件,当页面 DOM 加载完成后,执行一些获取 DOM ,设置 color-scheme 的逻辑。
dark mode 下 giscus 的处理
我希望 giscus 也会基于读者选择的 color-scheme 切换主题,博客中 giscus 是懒加载的,只有滚动到底部的时候才会加载。
从 Dynamic theme changing available? #336 中找到了动态切换主题的方法,即用 postMessage() 通知 giscus 切换主题,但这种方法只适用于手动点击 <select>
切换的情况。
除了读者主动切换 <select>
,我还需要在页面初始化的时候,还原读者之前选择的 color-scheme。
从 giscus 源码 看,giscus 初始化的时候也是通过 postMessage 去设置主题的,这里似乎存在一个竞态问题, gisucs 初始化的 postMessage 有时比我切换主题的 postMessage 执行得晚,会将我设置的主题重置。
为了避免这个问题,我通过 setTimeout() 延缓切换主题的 postMesssage 的执行,在很大程度上能够解决问题,基本很少出现被覆盖的情况。
dark mode 下 iframe 的处理
我的博客里会用 iframe 展示一些例子,<iframe> 是独立于我博客窗口的,不受 style.css 的影响,为了能够适配 color-scheme,我会遍历页面上所有的 iframe,然后在他们的 contentDocument 设置 color-scheme 属性。
这里也会存在一个时机的问题,需要在 iframe 加载完成后再设置 color-scheme 才行,不然 color-scheme 无法生效,实现上是通过监听 iframe 的 load event 进行设置的。
dark mode 下代码块高亮的处理
我的博客是用 org-publish 实现的,代码块的高亮颜色是 publish 时 Emacs 的主题颜色决定的,要么是亮色,要么是暗色,没法同时导出两种高亮主题。
而我默认导出的是一个亮色的主题,这就导致了在 dark mode 下,代码高亮会看不清,或者很刺眼。
publish 生成的代码高亮的颜色是单独写在 <span>
上的,没有类名,通过 CSS 也不方便选择元素设置颜色。
最开始的方案是统一导出暗色主题,这样 dark mode 下看起来是正常的,light mode 上就将代码块的背景色设置成 dark mode 的背景色,也能看清,就是页面上会有很多黑色的代码块,有点突兀。
后来突然想到可以用 CSS fitler 的 invert() 将颜色进行反转,那是不是可以直接把亮色主题的代码颜色,反转一下,就能在 dark mode 下用了? (≖ᴗ≖๑)
于是设置了 filter: invert(100%);
,在 dark mode 下代码确实能看清了,但是由于颜色反转过头,像是 diff 的颜色,绿色表示新增,红色表示移除,invert 之后已经不是这两个颜色了,不容易让人看明白。
但思路应该是可行的,于是我在结合其他 filter 的属性,最终得到了一个相对还能用的 filter:
@media (prefers-color-scheme: dark) { pre.src > *, pre.example > * { filter: invert(20%) brightness(200%); } } html:has(meta[name="color-scheme"][content="dark"]) pre.src > *, html:has(meta[name="color-scheme"][content="dark"]) pre.example > * { filter: invert(20%) brightness(200%); }
这样既复用了亮色主题的高亮,也能在 dark mode 下看得比较清楚,凑合能用了 (´・ω・`)
TODO Todos
除了上面的问题,其实还有一些没解决的:
[ ]
不同 color-scheme 下图片的处理,dark mode 下出现白底的图片,还是有点刺眼[ ]
一些 CSS 特性的兼容性测试,有的浏览器可能不兼容[ ]
稳定 iframe,giscus 的主题切换,避免切换失败的情况[ ]
更好看的 dark mode 配色
写在最后
所以现在博客有 dark mode 可以用啦 \m/ >_< \m/
你可以尝试看看,如果使用下来有什么问题,或者更好的建议,欢迎留言呀~