有时候我也很佩服自己,这么简单的一个功能,写写也就几个小时,一年多前就想搞了,竟然给我拖到现在才装上去。拖延症,恐怖如斯!

以前我对深色模式其实不怎么感冒,主要感觉开了也没啥用,就系统界面变黑了,其他 App 里还是白色的,等于没开。不过这几年大部分应用的适配都跟上来了,体验也就好起来了,晚上玩手机看着不那么刺眼,挺好的。

现在浏览器网页也支持检测用户的系统主题色,所以我也凑个热闹,给博客加上了自动切换浅色/深色主题的功能。适配过程还是挺顺利的,记录一下供参考。

原理

就是使用 CSS 的 prefers-color-scheme 媒体查询。

@media (prefers-color-scheme: dark) {
  /* dark theme styles go here */
}

参考文档:prefers-color-scheme - CSS | MDN

不过需要注意的是,不支持 IE

使用方法

最简单的例子:

body {
  background-color: white;
  color: black;
}

@media (prefers-color-scheme: dark) {
  body {
    background-color: black;
    color: white;
  }
}

这样在亮色模式下是白底黑字,在暗色模式下就是黑底白字。

依样画葫芦,给原主题中颜色相关的 CSS 加上对应的深色样式就差不多了。

使用 mixin 处理颜色

拿我自己写的这个主题举例,在主题中我们一般会用到很多颜色。一个常见的做法就是使用 CSS 预处理器,把这些颜色定义成变量方便后续使用(我用的是 Stylus):

$color-primary        = convert(hexo-config('primary_color'));
$color-background     = #fff;
$color-text           = #333;
$color-text-secondary = #999;

同样,定义这些颜色的深色版本:

$color-primary-dark        = convert(hexo-config('primary_color_dark'));
$color-background-dark     = #181a1b;
$color-text-dark           = #c8c3bc;
$color-text-secondary-dark = #a8a095;

引用之:

body {
  background-color: $color-background;
  color: $color-text;
}

a {
  color: $color-primary;
}

@media (prefers-color-scheme: dark) {
  body {
    background-color: $color-background-dark;
    color: $color-text-dark;
  }

  a {
    color: $color-primary-dark;
  }
}

然而问题来了,这样岂不是要写很多媒体查询语句?麻烦且不说,看着都眼花。如果把不同地方的这些语句集中起来,放在一起,又会破坏模块设计,也不利于后续维护。

想要写得简洁一点,不妨利用 CSS 预处理器的 mixin 特性

定义 mixin(可以理解为可重用的代码片段):

// 根据传入参数拼装变量名
color-themed(name) {
  color: lookup('$color-' + name);

  @media (prefers-color-scheme: dark) {
    color: lookup('$color-' + name + '-dark');
  }
}

这个 mixin 的意思就是我们传一个名称进去,它会根据这个名称去查找对应的颜色变量及其深色版本,然后一起应用。

如此一来,上面的样式就可以简化为:

body {
  background-color-themed: 'background';
  color-themed: 'text';
}

a {
  color-themed: 'primary';
}

使用 CSS 变量处理颜色

用上面那种方法,比原来的是好了不少,但感觉不太直观。

另一种方法,就是用 CSS 原生的变量机制来处理颜色。定义变量:

:root {
  --color-primary: #7065a3;
  --color-background: #fff;
  --color-text: #333;
  --color-text-secondary: #999;
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-primary: #bb86fc;
    --color-background: #181a1b;
    --color-text: #c8c3bc;
    --color-text-secondary: #a8a095;
  }
}

使用:

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

a {
  color: var(--color-primary);
}

是不是清爽了很多呢?

不过遗憾的是,IE 浏览器不支持 CSS 变量。(又是你!!!🙃

所以为了兼容性我还是选了预处理器 + mixin 的方法,这样在 IE 上虽然不能自动切换,但至少能保证默认的浅色主题是可以正常显示的。而如果全部使用 CSS 变量的话,在不支持的浏览器上就啥都没有了,得考虑 polyfill 和 fallback,还是算了。

如果不用考虑兼容旧浏览器的话,CSS 变量是最佳选择。

加载外部样式

使用 link 标签加载的外部 CSS 也可以指定媒体查询

比如本主题使用的 highlight.js 代码高亮的样式:

<link rel="stylesheet" href="atom-one-dark.min.css" media="screen and (prefers-color-scheme: dark)">
<link rel="stylesheet" href="atom-one-light.min.css" media="screen and (prefers-color-scheme: light)">

这样在浅色模式下会加载 light 样式,在深色模式下会加载 dark 样式。

参考

另外,关于深色模式下的图片要如何处理,其实也是需要考虑的。

不过我懒,就直接不管了。更详细的相关内容可以参考:

最后是自动切换的效果图(视频):