完美实现 Hugo 博客暗黑模式切换:从原理到实践

2025-09-09 00:00    #Hugo   #Web开发   #JavaScript   #CSS  

在当今的用户体验设计中,提供暗黑模式(Dark Mode)几乎已成为标配。它不仅能减少眼部疲劳,在低光环境下也更加舒适。本文将详细记录如何为一个 Hugo 网站(实际上适用于任何静态网站)添加一个功能完善、体验流畅的主题切换器。

我们将解决一个常见痛点:页面刷新时的“闪烁”问题,并最终实现一个堪称完美的解决方案。

最终效果

核心实现原理

我们的主题切换功能主要依赖两大现代 Web 技术的结合:

  1. CSS 自定义属性 (CSS Variables)
  2. 通过 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> 标签中。

这段脚本的任务非常纯粹:

  1. 检查 localStorage 中是否有用户之前保存的主题偏好。
  2. 如果没有,则检查用户的操作系统是否偏好深色模式。
  3. 根据结果,立即在 <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}

2. [data-theme='light'] — 浅色主题

1[data-theme='light'] {
2  --background: #ffffff;
3}

3. [data-theme='dark'] — 深色主题

1[data-theme='dark'] {
2  --background: #1b1b1b;
3  --text: #d8d8d8;
4  /* ... 其他变量 ... */
5}

data-theme 的工作原理

data-theme 是一个自定义数据属性 (Custom Data Attribute)。这是 HTML5 的一个很棒的功能,允许你将自定义的数据存储在 HTML 元素上,而不会影响其样式和行为。

它的工作原理就像一个状态开关 🚦,具体步骤如下:

  1. 定义样式:就像你上面提供的 CSS 代码一样,我们为不同的 data-theme 值(如 lightdark)定义不同的 CSS 变量。

  2. 设置初始状态:在 HTML 的根元素(通常是 <html>)上设置一个初始主题。

    1<html data-theme="light">
    2  </html>
    

    当页面加载时,浏览器看到 data-theme="light",于是 [data-theme='light'] 的 CSS 规则被应用,页面显示为浅色主题。

  3. 使用变量:在你的其他 CSS 文件中,你不会直接使用颜色代码,而是使用 var() 函数来调用这些变量。

    1body {
    2  background-color: var(--background); /* 使用 --background 变量 */
    3  color: var(--text); /* 使用 --text 变量 */
    4}
    5
    6a {
    7  color: var(--link); /* 使用 --link 变量 */
    8}
    

    这样做的好处是,你只需要改变变量的值,所有使用该变量的元素的样式都会自动更新。

  4. 用 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 属性作为开关,最后通过 JavaScript 来“拨动”这个开关,从而实现了一种非常现代化、高效且易于维护的网站主题切换方案。