给博客添加 dark mode

在整理周刊的时候,看到 Come to the light-dark() Side,一篇关于如何设置 dark mode 的很棒的文章。

我个人比较喜欢 light mode,除了代码编辑器外,其他都是 light mode。

我觉得 light mode 看起来比较舒服和自然,dark mode 下黑底白字比较刺眼。

但是文章里说到,不同人群有不同的需要:

不管如何,或许有人需要 dark mode,我也想实践一下文章中的方法,所以我给博客加上了 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 文档中的介绍,有两种办法去适配颜色:

  • 使用 prefers-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 fitlerinvert() 将颜色进行反转,那是不是可以直接把亮色主题的代码颜色,反转一下,就能在 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/

你可以尝试看看,如果使用下来有什么问题,或者更好的建议,欢迎留言呀~

Author: Spike Leung

Date: 2024-12-21 Sat 00:00

License: CC BY-NC 4.0