概述
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元素的darkclass - 深色模式:添加
html元素的darkclass - 自动模式:使用
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.darkclass 自动切换样式 - 使用 CSS 变量
--hue动态生成主题色
色相变量(HSL Color Model)
项目使用 HSL 颜色模型,只修改 Hue(色相)值:
background: hsl(var(--hue), 50%, 50%);这样用户调整滑块时,可以流畅地改变整个主题的色彩,同时保持饱和度和亮度不变。
数据持久化
localStorage 机制
系统使用 localStorage 存储两个值:
| 键名 | 描述 | 示例值 |
|---|---|---|
theme | 用户选择的主题模式 | "dark" / "light" / "auto" |
hue | 主题色相值 | "250" |
工作流程
-
首次访问:用户无 localStorage 记录
- 使用默认主题(Auto)
- 使用配置中的默认色相值
-
用户切换主题:
- 点击主题按钮 →
switchScheme() - 调用
setTheme()→ 保存到 localStorage - 调用
applyThemeToDocument()→ 更新 DOM
- 点击主题按钮 →
-
页面重新加载:
- 内联脚本从 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 的主题切换系统是一个精心设计的完整解决方案:
- 初始化层:内联脚本在页面渲染前应用主题,避免闪烁
- 数据持久化:localStorage 存储用户偏好
- 实时交互:Svelte 组件提供流畅的切换 UI
- CSS 动态化:CSS 变量 + Tailwind dark 模式实现自适应样式
- 系统集成:matchMedia 与操作系统主题同步
这套系统展示了现代 Web 应用中如何优雅地实现深色模式支持,值得学习和参考!
附录:DaisyUI 主题配置方案
如果你想在项目中集成 DaisyUI,可以基于 Fuwari 的 --hue 系统创建动态主题。以下是完整的配置方案:
1. 安装 DaisyUI
pnpm add -D daisyui2. 更新 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 代码,独立添加 |
最佳实践
- 保持色相同步:确保
--hue值在所有组件中统一 - 对比度检查:验证浅/深色模式下的文字对比度 (WCAG AA)
- 性能优化:使用 CSS 变量而非 JavaScript 动态修改样式
- 测试多个色相:验证从 0-360 的各个色相值下的显示效果
这套方案完全基于 Fuwari 的现有主题系统,可以无缝集成 DaisyUI 的丰富组件库!