web主题适配方案指北

前置知识

在这里了解实现网页主题切换的相关知识。

CSS 变量

要实现主题切换需要了解一点 css 自定义属性。当然,本文还提供了其他实现方式,为了不给您接下来的阅读带来阻碍,先了解它。

变量的声明

声明变量时,变量名前要加上 --,例如 --example: 20px 即是一个 css 声明语句。意思是将 20px 赋值给 --example 变量。所以 css 变量又叫做 css 自定义属性。在 css 的任何选择器中都可以声明 css 变量,通常将所有 css 变量声明在 :root 选择器中,以便在后文引用。

:root 选择器匹配文档树的根元素。对于 HTML 文档来说,:root 表示 <html> 元素,除了优先级更高之外,与 html 选择器相同。

这里有一个例子:

:root {
  --example: 20px
}

等价于:

html {
  --example: 20px
}

var()函数

  1. 通过 var() 函数读取变量。例如:var(--example) 会返回 --example所对应的值。

  2. var() 函数还可以使用第二个参数,表示变量的默认值。即 var() 从左向右读取值,如果第一个变量不存在,就读取第二个。例如:var(--example, 40px), 如果 --example 不存在,将返回 40px。当然第二个参数同样可以使用 css 自定义属性而不是具体的值,例如:var(--example1, --example2)

试着写一个简单的例子:

<body>
  <section id="container">
    <div class="item1"></div>
    <div class="item2"></div>
    <div class="item3"></div>
    <div class="item4"></div>
  </section>
</body>
#container {
  width: 400px;
  height: 150px;
  background-color: #ffeead;
  border: 1px solid #666;
  display: flex;
  flex-direction: row;
  justify-content: space-around;
  align-items: center;
}
#container > div {
  width: 70px;
  height: 50px;
}
#container div:nth-child(2n) {
  background-color: lightgreen;
}
#container div:nth-child(2n+1) {
  background-color: lightpink;
}

接下来使用 css 变量,修改部分代码:

+ :root {
+   --green: lightgreen;
+   --lightpink: lightpink;
+ }

#container {
  width: 400px;
  height: 150px;
  background-color: #ffeead;
  border: 1px solid #666;
  display: flex;
  flex-direction: row;
  justify-content: space-around;
  align-items: center;
}

#container > div {
  width: 70px;
  height: 50px;
}

#container div:nth-child(2n) {
- background-color: lightgreen;
+ background-color: var(--green);
}

#container div:nth-child(2n+1) {
- background-color: lightpink;
+ background-color: var(--lightpink);
}

在上面的代码片段中,使用 css 变量替换原来的颜色值,效果依然相同。css 变量还有许多其他相关知识,本文只介绍这些内容,只需要掌握这些,就能实现完整的暗色模式了。

兼容性

由图可见,如果不需要兼容 IE 浏览器,可以放心使用它。要兼容 IE 也有办法,postcss-css-variables 插件将 CSS 自定义属性 (CSS 变量) 语法转换为静态表示形式,具体使用方式本文不展开了,点我 查看详细的官方使用教程。

跟随系统设置

使用 css 媒体查询匹配系统设置。此处简单将 prefers-color-scheme CSS 媒体特性作介绍,参考MDNprefers-color-scheme用于检测用户是否有将系统的主题色设置为亮色或者暗色。

// 表示系统未得知用户在这方面的选项
@media (prefers-color-scheme: light) { }

// 用户选择选择使用浅色主题的系统界面
@media (prefers-color-scheme: dark) { }

// 用户选择选择使用暗色主题的系统界面
@media (prefers-color-scheme: no-preference) { }

使用 matchedMedia() 匹配系统设置。

const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');

if (prefersDarkScheme.matches) {
  document.body.classList.add('dark-theme');

} else {
  document.body.classList.remove('dark-theme');
}

使用这种方式有一个缺点:当这段 JavaScript 在 CSS 之后执行时,可能会出现一个闪屏的现象。

实现方式

有多种方式实现暗色模式,逐一介绍。

使用 css 变量

利用 var() 函数的特性实现浅色模式和暗色模式之间的切换。看下面的代码片段:

:root{
   --default-color: #555,
   --color: var(--dark-var, --default-var)
}

body{
  color: var(--color)
}

body 最终得到的 color 为 #555 ,如果声明了 —-dark-var 变量,body 得到的 color 的值将为 —-dark-var 的值,可以通过 JavaScript 将变量 —-dark-var 插入到 css 。

结合媒体查询实现跟随系统切换到深色模式:

@media (prefers-color-scheme: dark) {
  :root {
    --dark-color: #fff
  }
}

使用 html 属性和 css 变量

给 html 标签动态添加 theme 属性,像这样 <html theme="dark">

:root{
    --color: #555
}

:root[theme="dark"]{
    --color: #fff
}

body {
  color: var(--color)
}

当 html 的属性 theme 的值不为 dark 时,var() 函数读取的是 :root{} 内的自定义属性,反之,则读取的是 :root[theme="dark"]{} 里的自定义属性。

同样也可以结合媒体查询,实现跟随系统的效果:

@media (prefers-color-scheme: dark) {
  :root {
    --color: #fff
  }
}

这种方式的好处是扩展性更强,代码量更少,代码维护也更加方便。不仅仅可以切换到深色模式,还可以切换到其他主题。例如给 html 的 theme 属性设置其他值 <html theme="pink"> ,只需要添加下面这段 css:

:root[theme="pink"]{
    --color: pink
    // ...
}

通过 JavaScript 给 html 的 theme 属性赋值 pink ,就能切换到该主题了。

使用 class 和 css 变量

类似的思路我们可以给 html 添加一个 class 而不是添加 theme 属性实现。

::root{
  --color: #222;
}

:root.dark{
  --color: #eee;
}

const btn = document.querySelector('.toggle');

btn.addEventListener('click', function() {
  document.html.classList.toggle('dark');
})

用户设置深色模式的操作系统并不意味着他们希望将深色模式应用到网站上。结合媒体查询还可以实现:不管系统设置如何,覆盖深色模式。

body {
  --color: #222;
}

body.dark{
  --color: #eee;
}

@media (prefers-color-scheme: dark) {
  body {
    --color: #eee;
  }

  body.light {*
    *--color: #222;
  }
}

使用 class 作为标识

通过 JavaScript 改变 body 上的 class 来决定网站使用的主题。

<body class="dark || light">
const btn = document.querySelector('.toggle');

btn.addEventListener('click', function() {
  document.body.classList.toggle('dark');
})
body {
  color: #222;
  background: #fff;
}

body.dark{
  color: #eee;
  background: #121212;
}

使用这种方式时,我推荐使用 css 预处理器比如 scss,不然需要做很多重复的劳动。

使用单独的 css 文件

light-theme.css

body {
  color: #222;
  background: #fff;
}

dark-theme.css

body {
  color: #eee;
  background: #121212;
}

这时候你可能会有疑问了,如何通过点击切换主题呢?不用担心,非常简单。在引入 css 时这样做:

<head>
  <link href="light-theme.css" rel="stylesheet" id="theme-link">
</head>

link 一个标签一个 ID, 就可以通过 JavaScript 选择它了。

const btn = document.querySelector(".toggle");
const theme = document.querySelector("#theme-link");

btn.addEventListener("click", function() {
  if (theme.getAttribute("href") == "light-theme.css") {
    theme.href = "dark-theme.css";
  } else {
    theme.href = "light-theme.css";
  }
});

使用 Darkmode.js

GitHub 上有一个开源项目,Darkmode.js,通过mix-blend-mode:difference 达到切换夜间模式的效果,当您的应用或网站比较简单时或许可以尝试它。

npm install darkmode-js
const options = {
  bottom: '64px',
  right: 'unset',
  left: '32px',
  time: '0.5s',
  mixColor: '#fff',
  backgroundColor: '#fff',
  buttonColorDark: '#100f2c',
  buttonColorLight: '#fff',
  saveInCookies: false,
  label: '🌓',
  autoMatchOsTheme: true
}

const darkmode = new Darkmode(options);
darkmode.showWidget();

具体的原理我已经写了另一篇文章 Darkmode.js 源码解析 介绍它。

使用服务端脚本

以 PHP 为例:

<?php
$themeClass = '';
if (isset($_GET['theme']) && $_GET['theme'] == 'dark') {
  $themeClass = 'dark-theme';
}

$themeToggle = ($themeClass == 'dark-theme') ? 'light' : 'dark';
?>

<!DOCTYPE html>
<html lang="en">
<!-- etc. -->
<body class="<?php echo $themeClass; ?>">
  <a href="?theme=<?php echo $themeToggle; ?>">Toggle Dark Mode</a>
  <!-- etc. -->
</body>
</html>

实现切换单独的 css 文件:

<?php
$themeStyleSheet = 'light-theme.css';
if (isset($_GET['theme']) && $_GET['theme'] == 'dark') {
  $themeStyleSheet = 'dark-theme.css';
}

$themeToggle = ($themeStyleSheet == 'dark-theme.css') ? 'light' : 'dark';
?>

<!DOCTYPE html>
<html lang="en">
<head>
  <!-- etc. -->
  <link href="<?php echo $themeStyleSheet; ?>" rel="stylesheet">
</head>

<body>
  <a href="?theme=<?php echo $themeToggle; ?>">Toggle Dark Mode</a>
  <!-- etc. -->
</body>
</html>

这种方法有一个明显的缺点:需要刷新页面才能进行切换。但是,像这样的服务器端解决方案对于跨页面重新加载持久化用户的主题选择非常有用。

选择哪种方法

如果不需要兼容 IE,我推荐您使用 html 属性和 css 变量。或者,将不同方式结合使用也未尝不可。总之,应该根据您的项目需求选用更加合适的方法。

细节处理

图片

大多数网站不仅仅只有文本,还有图片。使用 filter 来处理图片。

img.dark{
  filter: brightness(.8) contrast(1.2);
}
  • brightness 使图像看起来更亮或更暗

  • contrast 调整图像的对比度

阴影

不要忽略暗色模式下的阴影(box-shadow),浅色模式的阴影在深色模式下可能无法显现。应该将这些细节也抽分,即应该为深色模式也提供一套阴影。

css 变量粒度

在抽离或者定义 css 变量时应该尽可能掌控粒度。粒度太大不好掌控细节,太小会增加代码量,不易维护。总之,视情况而定还有您的经验。

过渡效果

不要使用 transition

从暗色模式切换到浅色模式,或者从浅色模式切换到暗色模式,需要一个过渡动效,这能改善体验。您或许立即想到了transition,千万不要这么做。下面来看看这样做时有什么不足。

如果利用 css transition 属性,在您切换模式时应该给顶层元素一个 class,例如 mode-change ,在切换完成之后再将它移除。scss 代码大至如下:

.mode-change {
  selector1,
  selector2,
  // ......{
        transition: all 0.3s cubic-bezier(1, 0.05, 0.29, 0.99);
  }
}

这是因为:使用 transition,您需要给所有元素添加过渡效果。这将带来巨大的 CPU 开销,我已经测试过了。

障眼法

如果您常写 css 特效,障眼法真是个巧妙的方法,利用它实现很多难以实现的效果。现在,甚至用它来优化性能。不妨回归最初,目的是什么?给用户一个过渡效果,看起来不那么生硬。思路:使用 css 伪元素创建一个带有过渡效果的蒙层,看看如和实现它吧。

在切换时给 body 添加一个 class,在浅色切换到深色时 class 为 light-to-dark,反之,为 dark-to-light

代码较长,点击展开
$mode: () !default;
$mode: map-merge(
    (
        light-bg: #fff,
        dark-bg: #252528,
    ),
    $mode
);

$light-bg: map-get($mode, light-bg);
$dark-bg: map-get($mode, dark-bg);

.dark-to-light:after {
    content: '';
    width: 100vw;
    height: 100vh;
    position: fixed;
    z-index: 99999;
    left: 0;
    top: 0;
    margin-left: 0;
    background-color: $dark-bg;
    opacity: 0.7;
    animation: toLight 1s linear 0s forwards;
    // pointer-events: none;
}

.light-to-dark:after {
    content: '';
    width: 100vw;
    height: 100vh;
    position: fixed;
    z-index: 99999;
    left: 0;
    top: 0;
    margin-left: 0;
    background-color: $light-bg;
    opacity: 0.7;
    animation: toDark 1s linear 0s forwards;
    // pointer-events: none;
}

@keyframes toLight {
    0% {
        background-color: $dark-bg;
        opacity: 0.7;
    }
    100% {
        background-color: $light-bg;
        opacity: 0;
    }
}

@keyframes toDark {
    0% {
        background-color: $light-bg;
        opacity: 0.7;
    }
    100% {
        background-color: $dark-bg;
        opacity: 0;
    }
}

在切换模式时,将会在页面顶层展示带有对应过渡效果的蒙层。在过渡效果显示时,用户的鼠标无法点击页面的元素,这样做实现了类似防抖的效果。如果想移除这个效果,只需给蒙层加上 pointer-events: none; 。最终的效果就是:即使频繁切换模式,也听不见 CPU 狂飙。

储存状态

仅仅通过点击按钮切换主题还不够,应该将主题保存起来。否则,用户刷新页面或者再此进入页面将回到初始主题。在切换主题或者初始化时都应该使用状态储存。

JavaScript LocalStorage

在切换皮肤时应该将当前主题存到 LocalStorage。

localStorage.setItem("theme", "dark" || "light");
localStorage.getItem("theme");

$_COOKIE['theme'] == 'dark'

暗色模式设计最佳实践

我对设计和色彩不够专业,我找了一篇优秀的文章,您可以点此查看

参考资料: