在当今的用户体验设计中,提供暗黑模式(Dark Mode)几乎已成为标配。它不仅能减少眼部疲劳,在低光环境下也更加舒适。本文将详细记录如何为一个 Hugo 网站(实际上适用于任何静态网站)添加一个功能完善、体验流畅的主题切换器。
我们将解决一个常见痛点:页面刷新时的“闪烁”问题,并最终实现一个堪称完美的解决方案。
最终效果
- 一个位于导航栏的按钮,用于在浅色(Light)和深色(Dark)主题间切换。
- 用户的选择会被浏览器记住,下次访问时自动应用。
- 如果用户首次访问,则会根据其操作系统的偏好自动选择主题。
- 最重要的:页面在加载和刷新时绝不会出现恼人的颜色闪烁。
核心实现原理
我们的主题切换功能主要依赖两大现代 Web 技术的结合:
- CSS 自定义属性 (CSS Variables)
- 通过 JavaScript 操作 HTML 标签的
data-*属性
基本思路是:我们在 CSS 中定义两套颜色变量,一套用于浅色主题,另一套用于深色主题。然后,通过 JavaScript 动态地在 <html> 根元素上切换一个 data-theme 属性(例如,data-theme="light" 或 data-theme="dark")。CSS 会根据这个属性的值,自动应用对应的颜色变量集,从而实现整个网站的颜色切换。
1/* 基础/浅色主题变量 */
2:root {
3 --background: #ffffff;
4 --text: #000000;
5 --link: #3366cc;
6 /* ... 其他颜色变量 */
7}
8
9/* 深色主题变量 */
10[data-theme='dark'] {
11 --background: #1b1b1b;
12 --text: #d8d8d8;
13 --link: #88a9e9;
14 /* ... 其他颜色变量 */
15}
16
17/* 在组件中使用变量 */
18body {
19 background-color: var(--background);
20 color: var(--text);
21}
22
23a {
24 color: var(--link);
25}
这种方法的优势在于,我们无需加载不同的 CSS 文件,只需改变一个属性,就能高效地重绘整个页面的颜色,非常灵活且性能优秀。
恼人的“闪烁”问题及解决方案
很多主题切换方案都存在一个通病:当用户选择深色模式并刷新页面时,页面会先以默认的浅色模式短暂显示,然后“闪烁”一下再变成深色。
闪烁原因:这是因为负责切换主题的 JavaScript 脚本通常放在 <body> 底部加载。浏览器会先按照 CSS 的默认设置(通常是浅色主题)渲染页面,等 JS 加载并执行后,才把主题切换过来,这个过程就造成了闪烁。
解决方案:答案是在浏览器渲染 <body> 之前就确定好主题。我们将一小段关键的 JavaScript 代码作为内联脚本直接放在 <head> 标签中。
这段脚本的任务非常纯粹:
- 检查
localStorage中是否有用户之前保存的主题偏好。 - 如果没有,则检查用户的操作系统是否偏好深色模式。
- 根据结果,立即在
<html>标签上设置data-theme属性。
因为这段脚本在 <head> 中,它会比页面的任何可见元素(<body> 内的内容)都先执行,从而确保了浏览器在“第一帧”渲染时就使用了正确的主题,彻底消除了闪烁。
分步实现指南
第 1 步:在 header.html 中添加核心脚本和切换按钮
首先,我们需要将实现无闪烁切换的核心脚本和按钮添加到主题的 header.html 中。
1// filepath: layouts/partials/header.html
2
3<head>
4 {{/* ... 其他 head 内容 ... */}}
5
6 {{/* 核心:防止闪烁的内联脚本 */}}
7 <script>
8 // 在页面加载或切换主题时,此脚本应内联在 <head> 中以避免 FOUC (Flash of Unstyled Content)
9 (function() {
10 const theme = localStorage.getItem('theme') || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
11 document.documentElement.setAttribute('data-theme', theme);
12 })();
13 </script>
14
15 {{/* ... 其他 head 内容,如 CSS 链接等 ... */}}
16</head>
17
18<body>
19 <nav>
20 <ul class="menu">
21 {{/* ... 其他菜单项 ... */}}
22 <li><a href="/posts">文章</a></li>
23 {{/* 主题切换按钮 */}}
24 <li><button id="theme-switcher" class="menu-link" type="button">🌗</button></li>
25 </ul>
26 </nav>
27 {{/* ... */}}
第 2 步:创建处理按钮交互的 JavaScript
接下来,我们创建一个单独的 JS 文件来处理按钮的点击事件。这个脚本可以放在 <body> 底部加载,因为它不负责初始主题的设定,只负责响应用户操作。
1// filepath: static/js/theme-switcher.js
2
3(function() {
4 const themeSwitcher = document.getElementById('theme-switcher');
5 if (!themeSwitcher) {
6 return;
7 }
8
9 // 页面加载时,根据当前主题设置按钮图标
10 let currentTheme = document.documentElement.getAttribute('data-theme');
11 themeSwitcher.textContent = currentTheme === 'dark' ? '☀️' : '🌗';
12
13 // 监听按钮点击事件
14 themeSwitcher.addEventListener('click', () => {
15 // 切换主题
16 currentTheme = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
17 document.documentElement.setAttribute('data-theme', currentTheme);
18
19 // 将用户的选择存入 localStorage
20 localStorage.setItem('theme', currentTheme);
21
22 // 更新按钮图标
23 themeSwitcher.textContent = currentTheme === 'dark' ? '☀️' : '🌗';
24 });
25})();
别忘了在 footer_scripts.html 或类似的地方引入这个脚本:
<script src="{{ "js/theme-switcher.js" | relURL }}"></script>
第 3 步:配置 CSS 颜色变量
最后,也是最关键的一步,是配置好我们的 CSS 文件,让它能够响应 data-theme 属性的变化。
1/* filepath: assets/css/main.css */
2
3/* 默认/浅色主题变量 */
4:root {
5 --background: #ffffff;
6 --text: black;
7 --link: #3366cc;
8 --card-background: #f9f9f9;
9 /* ... 更多颜色 */
10}
11
12/* 当 data-theme='light' 时应用的变量(可选,用于明确指定) */
13[data-theme='light'] {
14 --background: #ffffff;
15 --text: black;
16 --link: #3366cc;
17 --card-background: #f9f9f9;
18 /* ... */
19}
20
21/* 当 data-theme='dark' 时应用的变量 */
22[data-theme='dark'] {
23 --background: #1b1b1b;
24 --text: #d8d8d8;
25 --link: #88a9e9;
26 --card-background: #272727;
27 /* ... 更多颜色 */
28}
29
30/* ... 之后的 CSS 规则全部使用 var() 来引用颜色 ... */
31body {
32 background-color: var(--background);
33 color: var(--text);
34}
35
36.subcategory-card {
37 background-color: var(--card-background);
38}
总结
通过将主题决策逻辑前置到 <head> 并结合 CSS 变量,我们构建了一个健壮、高效且用户体验极佳的主题切换系统。这个方案不仅解决了恼人的“闪烁”问题,其模块化的结构也使得未来的样式维护和扩展变得异常简单。希望这篇文章能对你有所帮助!
data-theme 自定义属性原理
1:root {
2 --complimentary: #815205;
3 --text: black;
4 --code: black;
5 --accent-color: #00aaff;
6 --accent-color-light: rgba(0, 170, 255, 0.1);
7 --card-background: #cccccc5f;
8 --card-background-hover: #6e6b6bff;
9 --card-text-color: #100101ff;
10 --shadow-color: rgba(0, 0, 0, 0.1);
11 --text-dim: #444;
12 --link: #3366cc;
13 --link-visited: #795cb2;
14 --code-background: #f3f5f7;
15 --code-border: #eaecf0;
16 --code-border: transparent;
17 --code-hl: #e6e6e6;
18 --border-general: #cccfd4;
19 --table-th: #e1e5ea;
20 --table-tr: #edeff2;
21 --toc-background: #f9f9f9;
22 --toc-border: #aaa;
23 --output-background: #d5dbe3;
24 --output-foreground: #805105;
25}
26
27[data-theme='light'] {
28 --background: #ffffff;
29}
30
31[data-theme='dark'] {
32 --background: #1b1b1b;
33 --text: #d8d8d8;
34 --code: #cbcbcb;
35 --accent-color: #00aaff;
36 --accent-color-light: rgba(0, 170, 255, 0.2);
37 --card-background: #272727;
38 --card-background-hover: #444;
39 --card-text-color: #d8d8d8;
40 --shadow-color: rgba(0, 0, 0, 0.3);
41 --text-dim: #a7a7a7;
42 --link: #88a9e9;
43 --link-visited: #a390ca;
44 --code-background: #272727;
45 --code-border: transparent;
46 --code-hl: #5a5a5a;
47 --border-general: #495057;
48 --table-th: #414141;
49 --table-tr: #343434;
50 --toc-background: #343434;
51 --toc-border: #495057;
52 --output-background: #020202;
53}
向我解释上面的代码,并向我解释 data-theme的原理
当然,我很乐意为你解释这段代码以及 data-theme 的工作原理。
简单来说,这段 CSS 代码使用 CSS 变量(也叫自定义属性)和 data- 属性来创建一套灵活的主题系统,可以轻松地在浅色模式 (light mode) 和深色模式 (dark mode) 之间切换。
代码解析
这段代码主要分为三个部分:
1. :root — 全局默认变量
1:root {
2 --complimentary: #815205;
3 --text: black;
4 /* ... 其他变量 ... */
5}
:root选择器:这个选择器指向文档的根元素,在 HTML 中就是<html>标签。在这里定义的变量是全局的,可以在整个页面的任何地方使用。- CSS 变量 (Custom Properties):以
--开头的都是 CSS 变量。你可以把它们看作是值的“占位符”。例如,--text: black;定义了一个名为text的变量,它的值是黑色。 - 作用:这个部分定义了网站的默认主题,或者说是“基础”颜色。当没有指定特定主题时,网站就会使用这些颜色。
2. [data-theme='light'] — 浅色主题
1[data-theme='light'] {
2 --background: #ffffff;
3}
[data-theme='light']:这是一个属性选择器。它的意思是:“选择任何带有data-theme属性并且该属性的值为light的元素”。- 作用:当
<html>或<body>标签被设置为<html data-theme="light">时,这个规则就会生效。它定义了浅色主题下的特定变量。在这里,它只定义了--background为白色 (#ffffff)。其他没有被重新定义的变量(如--text)会继续使用:root中的默认值。
3. [data-theme='dark'] — 深色主题
1[data-theme='dark'] {
2 --background: #1b1b1b;
3 --text: #d8d8d8;
4 /* ... 其他变量 ... */
5}
[data-theme='dark']:与上面类似,这个属性选择器会匹配<html data-theme="dark">这样的元素。- 作用:当网站切换到深色模式时,这个规则会生效。它会覆盖
:root中定义的许多变量,将它们替换为适合在深色背景下显示的颜色值(例如,背景变为深灰色,文字变为浅灰色)。
data-theme 的工作原理
data-theme 是一个自定义数据属性 (Custom Data Attribute)。这是 HTML5 的一个很棒的功能,允许你将自定义的数据存储在 HTML 元素上,而不会影响其样式和行为。
它的工作原理就像一个状态开关 🚦,具体步骤如下:
定义样式:就像你上面提供的 CSS 代码一样,我们为不同的
data-theme值(如light和dark)定义不同的 CSS 变量。设置初始状态:在 HTML 的根元素(通常是
<html>)上设置一个初始主题。1<html data-theme="light"> 2 </html>当页面加载时,浏览器看到
data-theme="light",于是[data-theme='light']的 CSS 规则被应用,页面显示为浅色主题。使用变量:在你的其他 CSS 文件中,你不会直接使用颜色代码,而是使用
var()函数来调用这些变量。1body { 2 background-color: var(--background); /* 使用 --background 变量 */ 3 color: var(--text); /* 使用 --text 变量 */ 4} 5 6a { 7 color: var(--link); /* 使用 --link 变量 */ 8}这样做的好处是,你只需要改变变量的值,所有使用该变量的元素的样式都会自动更新。
用 JavaScript 切换主题:当用户点击一个“切换主题”按钮时,你会用 JavaScript 来改变
<html>标签上data-theme属性的值。1// 获取 <html> 元素 2const htmlElement = document.documentElement; 3 4// 假设当前是 'light',我们要切换到 'dark' 5htmlElement.setAttribute('data-theme', 'dark');
切换瞬间发生了什么?
当 JavaScript 将 data-theme 的值从 'light' 改为 'dark' 时,浏览器会立即重新计算样式:
- 之前匹配的 CSS 规则
[data-theme='light']不再生效。 - 新的 CSS 规则
[data-theme='dark']开始生效。 - 所有 CSS 变量(
--background,--text等)的值被立刻更新为深色主题的值。 - 所有使用了
var()函数的元素(比如body)的样式都会自动重新渲染,页面瞬间就从浅色模式变成了深色模式,无需刷新页面。
总结
总而言之,这种方法通过 CSS 变量将设计系统的颜色集中管理,然后利用 data-theme 属性作为开关,最后通过 JavaScript 来“拨动”这个开关,从而实现了一种非常现代化、高效且易于维护的网站主题切换方案。