抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

暗色模式(Dark Mode)是近些年来掀起的风潮,通常暗黑模式是一种文字为浅色、背景为深色的应用程序模式。这和近些年来手机等移动设备上更多的 OLED 屏幕也有一定关系,因为 OLED 的工作原理,大面积的深色背景可以使其功耗更低。

另外,暗色模式也并不是近些年来才出现的一种模式。早期的计算机显示器多数就是黑底绿字,据说是因为当时 CRT 的工作原理导致显式黑色背景更为方便。

但不管具体来历,从移动端到桌面端,现代操作系统都具有系统级别的颜色切换。所谓的系统级别就是可以让不同的应用程序根据系统当前的显式模式来切换自身的显式模式。

Windows上的颜色切换

给网页添加暗色模式

可能是最近的暗色模式的风潮的影响,CSS 媒体查询也提供了一个强大的特性。Media Queries Level 5 (csswg.org) 中提出了深色模式的判断方式,CSS 媒体查询@media (prefers-color-scheme: value)。其中包含 light、dark 和no-preference三种值。

该媒体查询是根据系统的显式模式来进行切换的,也就是说 CSS 媒体查询通过浏览器为我们和系统建立了沟通的桥梁。例如当支持的系统的切换为深色模式时,@media (prefers-color-scheme: dark)就会被触发,我们可以根据修改其中的 CSS 变量,或者维护另一套 CSS 样式表来实现切换。

使用 CSS 变量

使用 CSS 变量的方式来实现切换的优点是代码量较少,可以快速切换到指定模式。但兼容性较差,市面上任然存在不支持 CSS 变量的浏览器。

/* variables */
:root {
  --text-color: #333;
  --main-background: #fff;
}

/* media query */
@media (prefers-color-scheme: dark) {
  :root {
    --text-color: rgb(255, 152, 0);
    --main-background: #333;
  }
}

条件加载样式表

<link>标签也支持使用媒体查询来进行条件加载样式表。这样的好处是不需要使用到 CSS 变量,但缺点就是需要维护两套样式表。

<link rel="stylesheet" href="main.css">
<link rel="stylesheet" href="dark.css" media="(prefers-color-scheme: dark)">

手动切换

还有种兼容性最好的切换方式,由用户手动触发对根标签的样式切换,从而达到切换暗色模式。并且还能将状态维护在 localStorage中,实现长期保存。这种方法并不复杂,缺点就是不能和系统进行沟通,和系统一同切换。

体验之上

如何才能有更好的模式切换体验呢?上述的切换方式都是要么根据系统来进行切换,要么用户没法手动进行设置。两种方式仿佛都差了点意思。

如果将两种方式结合到一起,那体验乃会更好。

将两种方式结合到一起不能仅仅使用手动模式来覆盖prefers-color-scheme: dark,当覆盖了之后会被存储到localStorage中,这样就永久的失去了自动模式。

这里的思路是,默认情况下遵循系统设置。当用户手动切换时覆盖系统设置,而当切换后和系统设置一致时,则恢复为系统设置。

先布局一下

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Dark Mode</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <h1 class="title">Hi, there👋</h1>
    <button class="switch-mode"></button>
  </body>
  <script src="index.js"></script>
</html>

为了测试,这里是一个简单的常规布局,分别使用了一个<h1><button>标签来展示和切换模式。

呈上 CSS

/* variables */
:root {
  --text-color: #333;
  --main-background: #fff;
}

/* media query */
@media (prefers-color-scheme: dark) {
  /* When prefers-color-scheme is dark mode (system level) */
  /* Avoid overwriting user custom color mode */
  :root:not([data-theme]) {
    --text-color: rgb(255, 152, 0);
    --main-background: #333;
  }
}

/* User custom color mode is dark */
[data-theme='dark'] {
  --text-color: rgb(255, 152, 0);
  --main-background: #333;
}

CSS 部分并不是特别复杂,且它主要是利用了媒体查询来与系统沟通。这里使用了一个伪类:not(),它的作用是不选中特定的类。当元素上有[data-theme='dark']属性时,利用伪类:not([data-theme])可以保证手动设置覆盖系统设置。通俗点来说,就是没有[data-theme]的元素就应用上媒体查询。

紧接着就是 JS

CSS 其实已经帮我们解决一大半的问题了。接下来还需要利用到 JavaScript 来覆盖系统设置与判断何时恢复系统设置(还有存储到localStorage)。

先来几个常量,方便后续直接使用。

const ROOT_ELEMENT = document.documentElement;
const STORAGE_KEY = 'color-mode';
const DATA_THEME = 'data-theme';

这里使用了类 C 的命名方式,他们分别是:

  • ROOT_ELEMENT:根元素;
  • STORAGE_KEY:存储到localStorage中的 key;
  • DATA_THEME:Attribute name。

先不要着急,为了方便,再定义几个操作localStorage的方法:

const setMode = (k: string, v: string) => {
  try {
    localStorage.setItem(k, v);
  } catch (e) {}
};

const removeMode = (k: string) => {
  try {
    localStorage.removeItem(k);
  } catch (e) {}
};

const getMode = (k: string) => {
  try {
    return localStorage.getItem(k);
  } catch (e) {
    return null;
  }
};

后续有多个地方需要用到同样的操作,所以利用try{...}catch{...}简单的封装了一下。

基(tou)本(lan)步骤做完了,根据之前的思路,接下来就是先判断当前文档是否处于系统设置中的暗色模式。

这里发现一个非常好用的 API matchMedia。它可以检查对应的媒体查询是否匹配,我们可以利用它来判断是否触发媒体查询,从而判断是否为系统设置下的暗色模式。

const isDarkMode = () => {
  return window.matchMedia('(prefers-color-scheme: dark)').matches
    ? 'dark'
    : 'light';
};

这样就能够轻松判断是否处于系统设置下的暗色模式了。先不着急,接下来先为重置封装一个函数:

const resetMode = () => {
  ROOT_ELEMENT.removeAttribute(DATA_THEME);
  removeMode(STORAGE_KEY);
};

该函数的作用是,当用户手动设置的目标模式与系统设置相同时,清楚 DOM 上的属性选择器与localStorage中的数据。恢复到系统设置中。

现在我们能确定是否处于系统设置中和能够清除手动设置了。接下来就是根据当前的模式判断下一次应该设置什么模式,以及是否恢复系统设置。

在封装下一个函数之前,得先需要创建两个对象,用于验证和反转对应的值:

type validKeys = {
  [key: string]: true;
};
const validKeys: validKeys = {
  dark: true,
  light: true,
};

type invertKeys = {
  [key: string]: 'dark' | 'light';
};
const inverKeys: invertKeys = {
  dark: 'light',
  light: 'dark',
};

现在我们来封装一个获取下次暗色模式的值:

const getColorMode = () => {
  // Fetch value from localStorage
  const currentSetting = getMode(STORAGE_KEY);
  let mode: 'dark' | 'light' | undefined;
  // If has value
  if (currentSetting && validKeys[currentSetting]) {
    // Invert it, get next mode
    mode = inverKeys[currentSetting];
  } else if (currentSetting === null) {
    // If has not value, inver media query
    mode = inverKeys[isDarkMode()];
  } else {
    return;
  }
  // Set value to localStorage
  setMode(STORAGE_KEY, mode);
  return mode;
};

这个方法首先从localStorage中取出暗色模式的值。如果能够取到,那就说明上一次的值成功被存入,下一次的值直接取反就行了。

如果没有取到值,那就说明上次被恢复了系统设置,或者用户是第一次访问文档。这时下次的值应该为系统设置取反。

两个简单的判断之后,就能确定下次该设置什么值了

确定了下次该设置的值之后,就该真的去设置了。这里通过封装一个函数来为根元素设置元素选择器或者恢复为系统设置。

const applyMode = (mode: string | null = getMode(STORAGE_KEY)) => {
  if (mode === isDarkMode()) {
    resetMode();
  } else if (mode && validKeys[mode]) {
    ROOT_ELEMENT.setAttribute(DATA_THEME, mode);
  } else {
    resetMode();
  }
};

这个函数相比较上一个更加简单,几个简单的判断就好了。如果下一次的模式与系统设置相同,那么就直接恢复系统设置。如果与系统设置不同,那么就要覆盖系统设置,这时就要为根元素添加对应的属性选择器。

如果出现其他什么奇奇怪怪的情况,就先恢复系统设置再说。

这里的参数使用了默认参数,从localStorage取出保存的模式值。这样做的目的是为了当文档第一次加载时,直接运行applyMode()函数,来为用户设置上次设置的模式。通俗点来说就是刷新后恢复上次的手动设置。

当然这也需要将暗色模式的 JavaScript 代码添加到<head>标签里,这样才能保证不会出现奇怪的闪屏。

  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Dark Mode</title>
    <link rel="stylesheet" href="style.css" />
    <script src="darkMode.js"></script>
  </head>

Demo

参考

你好黑暗,我的老朋友 —— 为网站添加用户友好的深色模式支持 | Sukka’s Blog (skk.moe)

评论