2184 字
11 分钟
Fuwari 博客主题切换机制深度解析

概述#

Fuwari 博客模板实现了一套完整的主题切换系统,支持浅色模式(Light)深色模式(Dark)自动模式(Auto) 三种主题切换方式。这个系统巧妙地结合了 localStorage、DOM 操作、CSS 变量和媒体查询,提供了流畅的用户体验。

核心架构#

主题切换系统由以下几个核心部分组成:

1. 常量定义(Constants)#

文件位置src/constants/constants.ts:3-6

export const LIGHT_MODE = "light",
DARK_MODE = "dark",
AUTO_MODE = "auto";
export const DEFAULT_THEME = AUTO_MODE;

定义了三种主题模式的字符串常量,默认主题为自动模式。

2. 设置工具函数(Setting Utils)#

文件位置src/utils/setting-utils.ts

这是主题系统的核心,提供了四个关键函数:

getStoredTheme()#

从 localStorage 获取用户保存的主题偏好,如果没有保存则返回默认主题。

export function getStoredTheme(): LIGHT_DARK_MODE {
return (localStorage.getItem("theme") as LIGHT_DARK_MODE) || DEFAULT_THEME;
}

setTheme(theme)#

保存主题选择到 localStorage,并立即应用到文档。

export function setTheme(theme: LIGHT_DARK_MODE): void {
localStorage.setItem("theme", theme);
applyThemeToDocument(theme);
}

applyThemeToDocument(theme)#

这是最关键的函数,根据选择的主题模式修改 DOM:

export function applyThemeToDocument(theme: LIGHT_DARK_MODE) {
switch (theme) {
case LIGHT_MODE:
document.documentElement.classList.remove("dark");
break;
case DARK_MODE:
document.documentElement.classList.add("dark");
break;
case AUTO_MODE:
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
break;
}
document.documentElement.setAttribute("data-theme", expressiveCodeConfig.theme);
}

核心原理

  • 浅色模式:移除 html 元素的 dark class
  • 深色模式:添加 html 元素的 dark class
  • 自动模式:使用 window.matchMedia("(prefers-color-scheme: dark)") 获取系统偏好

getHue()setHue(hue)#

管理主题色相值(0-360),用于动态改变主题颜色。

export function setHue(hue: number): void {
localStorage.setItem("hue", String(hue));
const r = document.querySelector(":root") as HTMLElement;
if (!r) return;
r.style.setProperty("--hue", String(hue));
}

实现细节#

初始化流程(Layout.astro)#

文件位置src/layouts/Layout.astro:113-139

为了避免”闪烁问题”(Flash of Unstyled Content, FOUC),主题设置在页面加载时作为内联脚本执行:

<script is:inline define:vars={{DEFAULT_THEME, LIGHT_MODE, DARK_MODE, AUTO_MODE, configHue}}>
// 从 localStorage 加载主题
const theme = localStorage.getItem('theme') || DEFAULT_THEME;
switch (theme) {
case LIGHT_MODE:
document.documentElement.classList.remove('dark');
break
case DARK_MODE:
document.documentElement.classList.add('dark');
break
case AUTO_MODE:
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}
// 加载主题色相值
const hue = localStorage.getItem('hue') || configHue;
document.documentElement.style.setProperty('--hue', hue);
</script>

为什么使用内联脚本?

  • 脚本在 HTML 解析过程中立即执行,在渲染前应用主题
  • 防止用户看到未应用主题的闪烁页面

用户交互(LightDarkSwitch.svelte)#

文件位置src/components/LightDarkSwitch.svelte

这是前端交互组件,提供主题切换的 UI:

const seq: LIGHT_DARK_MODE[] = [LIGHT_MODE, DARK_MODE, AUTO_MODE];
function toggleScheme() {
let i = 0;
for (; i < seq.length; i++) {
if (seq[i] === mode) break;
}
switchScheme(seq[(i + 1) % seq.length]);
}
function switchScheme(newMode: LIGHT_DARK_MODE) {
mode = newMode;
setTheme(newMode);
}

特点

  • 提供循环切换(Light → Dark → Auto → Light)
  • 支持系统主题变化时自动重新应用主题
  • 使用 matchMedia 监听系统偏好变化
onMount(() => {
mode = getStoredTheme();
const darkModePreference = window.matchMedia("(prefers-color-scheme: dark)");
darkModePreference.addEventListener("change", (_e) => {
applyThemeToDocument(mode);
});
});

主题色相选择(DisplaySettings.svelte)#

文件位置src/components/widget/DisplaySettings.svelte

允许用户通过滑块调整主题色相值(0-360):

<input type="range" min="0" max="360" bind:value={hue}
class="slider" step="5">

使用 Svelte 的响应式特性:

$: if (hue || hue === 0) {
setHue(hue);
}

CSS 样式系统#

主题变量定义(main.css)#

文件位置src/styles/main.css

使用 CSS 变量和 dark: 修饰符实现主题切换:

.btn-plain {
@apply transition relative flex items-center justify-center bg-none
text-black/75 hover:text-[var(--primary)] dark:text-white/75 dark:hover:text-[var(--primary)];
}

关键要点:

  • 利用 Tailwind CSS 的 dark: 修饰符
  • 根据 html.dark class 自动切换样式
  • 使用 CSS 变量 --hue 动态生成主题色

色相变量(HSL Color Model)#

项目使用 HSL 颜色模型,只修改 Hue(色相)值:

background: hsl(var(--hue), 50%, 50%);

这样用户调整滑块时,可以流畅地改变整个主题的色彩,同时保持饱和度和亮度不变。

数据持久化#

localStorage 机制#

系统使用 localStorage 存储两个值:

键名描述示例值
theme用户选择的主题模式"dark" / "light" / "auto"
hue主题色相值"250"

工作流程#

  1. 首次访问:用户无 localStorage 记录

    • 使用默认主题(Auto)
    • 使用配置中的默认色相值
  2. 用户切换主题

    • 点击主题按钮 → switchScheme()
    • 调用 setTheme() → 保存到 localStorage
    • 调用 applyThemeToDocument() → 更新 DOM
  3. 页面重新加载

    • 内联脚本从 localStorage 读取值
    • 在页面渲染前应用主题

系统偏好检测#

matchMedia API#

自动模式使用 window.matchMedia() 检测系统偏好:

window.matchMedia("(prefers-color-scheme: dark)").matches

特点

  • 实时监听系统主题变化
  • 用户改变系统设置时自动更新
  • 跨浏览器兼容性好
const darkModePreference = window.matchMedia("(prefers-color-scheme: dark)");
darkModePreference.addEventListener("change", (_e) => {
applyThemeToDocument(mode);
});

核心设计优势#

特性优势
内联脚本初始化避免页面闪烁,提升用户体验
localStorage 持久化用户偏好跨会话保存
matchMedia 监听自动响应系统主题变化
CSS 变量 + Tailwind动态主题色实现简洁高效
HSL 色相模型调整色相时保持颜色协调
三模式切换灵活满足不同用户需求

总结#

Fuwari 的主题切换系统是一个精心设计的完整解决方案:

  1. 初始化层:内联脚本在页面渲染前应用主题,避免闪烁
  2. 数据持久化:localStorage 存储用户偏好
  3. 实时交互:Svelte 组件提供流畅的切换 UI
  4. CSS 动态化:CSS 变量 + Tailwind dark 模式实现自适应样式
  5. 系统集成:matchMedia 与操作系统主题同步

这套系统展示了现代 Web 应用中如何优雅地实现深色模式支持,值得学习和参考!


附录:DaisyUI 主题配置方案#

如果你想在项目中集成 DaisyUI,可以基于 Fuwari 的 --hue 系统创建动态主题。以下是完整的配置方案:

1. 安装 DaisyUI#

Terminal window
pnpm add -D daisyui

2. 更新 Tailwind 配置(tailwind.config.cjs)#

/** @type {import('tailwindcss').Config} */
const defaultTheme = require("tailwindcss/defaultTheme")
module.exports = {
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue,mjs}"],
darkMode: "class",
theme: {
extend: {
fontFamily: {
sans: ["Roboto", "sans-serif", ...defaultTheme.fontFamily.sans],
},
},
},
plugins: [
require("@tailwindcss/typography"),
require("daisyui")
],
daisyui: {
themes: [
{
fuwari_light: {
"primary": "oklch(0.70 0.14 var(--hue))",
"primary-focus": "oklch(0.65 0.15 var(--hue))",
"primary-content": "#ffffff",
"secondary": "oklch(0.75 0.12 var(--hue))",
"secondary-focus": "oklch(0.70 0.13 var(--hue))",
"secondary-content": "#ffffff",
"accent": "oklch(0.78 0.10 var(--hue))",
"accent-focus": "oklch(0.73 0.11 var(--hue))",
"accent-content": "#ffffff",
"neutral": "#2b3544",
"neutral-focus": "#16a34a",
"neutral-content": "#ffffff",
"base-100": "oklch(0.95 0.01 var(--hue))",
"base-200": "oklch(0.90 0.01 var(--hue))",
"base-300": "oklch(0.85 0.01 var(--hue))",
"base-content": "#1f2937",
"info": "oklch(0.70 0.14 200)",
"success": "oklch(0.70 0.14 120)",
"warning": "oklch(0.70 0.14 60)",
"error": "oklch(0.70 0.14 0)",
"info-content": "#ffffff",
"success-content": "#ffffff",
"warning-content": "#ffffff",
"error-content": "#ffffff",
},
fuwari_dark: {
"primary": "oklch(0.75 0.14 var(--hue))",
"primary-focus": "oklch(0.80 0.13 var(--hue))",
"primary-content": "#ffffff",
"secondary": "oklch(0.72 0.13 var(--hue))",
"secondary-focus": "oklch(0.77 0.12 var(--hue))",
"secondary-content": "#ffffff",
"accent": "oklch(0.68 0.11 var(--hue))",
"accent-focus": "oklch(0.73 0.10 var(--hue))",
"accent-content": "#ffffff",
"neutral": "#d1d5db",
"neutral-focus": "#4ade80",
"neutral-content": "#1f2937",
"base-100": "oklch(0.16 0.014 var(--hue))",
"base-200": "oklch(0.20 0.012 var(--hue))",
"base-300": "oklch(0.25 0.010 var(--hue))",
"base-content": "#f3f4f6",
"info": "oklch(0.75 0.14 200)",
"success": "oklch(0.75 0.14 120)",
"warning": "oklch(0.75 0.14 60)",
"error": "oklch(0.65 0.20 25)",
"info-content": "#ffffff",
"success-content": "#ffffff",
"warning-content": "#ffffff",
"error-content": "#ffffff",
}
}
]
}
}

3. CSS 变量映射(src/styles/daisyui-override.css)#

创建一个新的样式文件来增强 DaisyUI 主题与 Fuwari 的集成:

/* 浅色模式变量 */
:root {
/* 主题色 */
--dui-primary-hue: var(--hue);
--dui-primary-sat: 14%;
--dui-primary-light: 70%;
/* 文本颜色 */
--dui-text-dark: #1f2937;
--dui-text-light: #f3f4f6;
/* 背景色 */
--dui-bg-light: oklch(0.95 0.01 var(--hue));
--dui-bg-light-secondary: oklch(0.90 0.01 var(--hue));
--dui-bg-light-tertiary: oklch(0.85 0.01 var(--hue));
}
:root.dark {
/* 暗色模式覆盖 */
--dui-primary-light: 75%;
--dui-text-dark: #f3f4f6;
--dui-text-light: #1f2937;
--dui-bg-light: oklch(0.16 0.014 var(--hue));
--dui-bg-light-secondary: oklch(0.20 0.012 var(--hue));
--dui-bg-light-tertiary: oklch(0.25 0.010 var(--hue));
}
/* 按钮样式增强 */
.btn {
transition: all 0.3s ease;
}
.btn-primary {
background-color: oklch(var(--dui-primary-light) 0.14 var(--hue));
border-color: oklch(var(--dui-primary-light) 0.14 var(--hue));
}
.btn-primary:hover {
background-color: oklch(calc(var(--dui-primary-light) - 5%) 0.15 var(--hue));
border-color: oklch(calc(var(--dui-primary-light) - 5%) 0.15 var(--hue));
}
/* 卡片样式 */
.card {
background-color: var(--dui-bg-light-secondary);
color: var(--dui-text-dark);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.005);
}
:root.dark .card {
color: var(--dui-text-light);
box-shadow: none;
}
/* 输入框样式 */
.input, .textarea, .select {
background-color: white;
color: var(--dui-text-dark);
border-color: oklch(0.90 0.01 var(--hue));
}
:root.dark .input,
:root.dark .textarea,
:root.dark .select {
background-color: oklch(0.20 0.012 var(--hue));
color: var(--dui-text-light);
border-color: oklch(0.25 0.010 var(--hue));
}
.input:focus, .textarea:focus, .select:focus {
border-color: oklch(0.70 0.14 var(--hue));
outline: none;
box-shadow: 0 0 0 3px oklch(0.70 0.14 var(--hue) / 0.1);
}
:root.dark .input:focus,
:root.dark .textarea:focus,
:root.dark .select:focus {
border-color: oklch(0.75 0.14 var(--hue));
box-shadow: 0 0 0 3px oklch(0.75 0.14 var(--hue) / 0.2);
}

4. 在 Layout.astro 中引入#

---
import "@styles/daisyui-override.css";
---
<html>
<!-- ... -->
</html>

5. 使用示例#

基础按钮#

<button class="btn btn-primary">主题色按钮</button>
<button class="btn btn-secondary">次要颜色</button>
<button class="btn btn-accent">强调色</button>

卡片组件#

<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title">卡片标题</h2>
<p>卡片内容,会自动响应主题色变化。</p>
</div>
</div>

输入表单#

<input type="text" placeholder="输入框会响应主题色" class="input input-bordered w-full" />
<textarea class="textarea textarea-bordered" placeholder="文本框"></textarea>

6. 动态主题色调整脚本增强#

如果需要在改变 --hue 后立即更新 DaisyUI 主题,可以在 setting-utils.ts 中添加:

export function setHueWithDaisyUI(hue: number): void {
localStorage.setItem("hue", String(hue));
const r = document.querySelector(":root") as HTMLElement;
if (!r) return;
r.style.setProperty("--hue", String(hue));
// 触发 DaisyUI 主题更新(如果需要重新计算)
document.dispatchEvent(new CustomEvent("theme:hue-changed", {
detail: { hue }
}));
}

核心优势#

特性说明
动态色相使用 var(--hue) 实现全局主题色动态变化
浅/深色模式DaisyUI 主题自动适配 html.dark class
OKLCH 颜色空间与 Fuwari 原有设计保持一致
自定义覆盖可通过 CSS 变量进一步定制样式
渐进式增强无需改动现有 Fuwari 代码,独立添加

最佳实践#

  1. 保持色相同步:确保 --hue 值在所有组件中统一
  2. 对比度检查:验证浅/深色模式下的文字对比度 (WCAG AA)
  3. 性能优化:使用 CSS 变量而非 JavaScript 动态修改样式
  4. 测试多个色相:验证从 0-360 的各个色相值下的显示效果

这套方案完全基于 Fuwari 的现有主题系统,可以无缝集成 DaisyUI 的丰富组件库!

Fuwari 博客主题切换机制深度解析
https://martinlevine.vercel.app/posts/theme-switching-mechanism/
作者
404 Not Found.
发布于
2025-10-23
许可协议
CC BY-NC-SA 4.0