本文主题聚焦于现代前端开发中一个高频痛点问题——如何优雅地实现“深色/浅色模式切换”,并做到系统偏好自动适配 + 用户手动覆盖 + 持久化记忆。文章包含完整代码、原理讲解与最佳实践。
🌓 为什么深色模式如此重要?
随着 macOS、Windows、iOS 和 Android 全面支持系统级深色模式,用户对网站/应用的视觉舒适度提出了更高要求。一个好的深色模式实现应满足:
- 尊重用户系统偏好(通过
prefers-color-scheme媒体查询); - 允许用户手动覆盖(提供切换按钮);
- 记住用户选择(刷新后不丢失);
- 无闪烁加载(避免页面先亮后暗的“FOUC”问题)。
本文将带你用 纯 HTML/CSS/JavaScript 实现一套符合上述所有要求的解决方案,代码简洁、兼容性好、可直接用于生产环境。
🧱 第一步:HTML 结构
我们只需要一个切换按钮和基本页面内容:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>智能深色模式示例</title>
<!-- 关键:内联脚本必须放在 CSS 之前,防止闪烁 -->
<script>
// 🚨 防闪烁关键代码(见下文详解)
(function() {
const stored = localStorage.getItem('color-scheme');
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const isDark = stored === 'dark' || (stored === null && systemPrefersDark);
if (isDark) document.documentElement.classList.add('dark');
})();
</script>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<header>
<h1>欢迎来到我的网站</h1>
<button id="theme-toggle" aria-label="切换主题">
<span class="sun-icon">☀️</span>
<span class="moon-icon">🌙</span>
</button>
</header>
<main>
<p>这是一个支持智能深色/浅色模式的页面。</p>
<p>你的选择将被保存,下次访问时自动生效。</p>
</main>
<script src="script.js"></script>
</body>
</html>
⚠️ 注意:
<script>内联代码必须放在<link rel="stylesheet">之前,这是防止“主题闪烁”的关键!
🎨 第二步:CSS 实现双主题
使用 CSS 自定义属性(变量)+ .dark 类来切换主题:
/* style.css */
:root {
--bg: #ffffff;
--text: #111111;
--border: #dddddd;
--button-bg: #f0f0f0;
}
.dark {
--bg: #121212;
--text: #e0e0e0;
--border: #333333;
--button-bg: #2d2d2d;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: var(--bg);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
transition: background-color 0.3s, color 0.3s; /* 平滑过渡 */
line-height: 1.6;
padding: 2rem;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
button#theme-toggle {
background: var(--button-bg);
border: 1px solid var(--border);
border-radius: 50%;
width: 40px;
height: 40px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
/* 隐藏当前不显示的图标 */
.dark .sun-icon { display: none; }
:not(.dark) .moon-icon { display: none; }
main {
max-width: 600px;
}
⚙️ 第三步:JavaScript 控制逻辑
// script.js
const toggleButton = document.getElementById('theme-toggle');
function setTheme(isDark) {
if (isDark) {
document.documentElement.classList.add('dark');
localStorage.setItem('color-scheme', 'dark');
} else {
document.documentElement.classList.remove('dark');
localStorage.setItem('color-scheme', 'light');
}
}
// 初始化:根据 localStorage 或系统偏好设置主题
function initTheme() {
const stored = localStorage.getItem('color-scheme');
if (stored === 'dark' || stored === 'light') {
setTheme(stored === 'dark');
} else {
// 未设置过,遵循系统偏好
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
setTheme(prefersDark);
}
}
// 切换按钮事件
toggleButton.addEventListener('click', () => {
const isCurrentlyDark = document.documentElement.classList.contains('dark');
setTheme(!isCurrentlyDark);
});
// 可选:监听系统偏好变化(当用户在 OS 中切换主题时自动更新)
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
const stored = localStorage.getItem('color-scheme');
// 仅当用户未手动设置时,才跟随系统变化
if (stored === null) {
setTheme(e.matches);
}
});
// 初始化
initTheme();
🔍 关键点解析
1. 为什么内联脚本要放在 CSS 之前?
如果先加载 CSS,浏览器会先渲染默认(浅色)样式,等 JS 执行后再切换为深色——造成明显的“闪烁”。
将判断逻辑放在 <head> 最顶部的内联 <script> 中,可在 CSS 加载前就给 <html> 添加 .dark 类,从而让 CSS 直接应用深色变量,彻底消除闪烁。
2. localStorage vs 系统偏好优先级
- 用户从未操作 → 跟随系统;
- 用户手动切换过 → 以用户选择为准,不再响应系统变化;
- 若希望始终跟随系统,可移除
localStorage存储逻辑。
3. 无障碍(a11y)考虑
- 为按钮添加
aria-label; - 使用语义化 HTML;
- 图标用 emoji 或 SVG(本文简化使用 emoji)。
🧪 测试建议
- 在 macOS / Windows 中切换系统主题,观察未设置时的行为;
- 点击按钮切换,刷新页面,确认状态保留;
- 清除 localStorage(
localStorage.clear()),验证是否回归系统偏好。
🚀 进阶优化方向
| 功能 | 实现方式 |
|---|---|
| 动画过渡更细腻 | 使用 @media (prefers-reduced-motion: no-preference) 包裹 transition |
| 支持更多主题(如 sepia) | 扩展为多值状态('light' / 'dark' / 'auto') |
| SSR 支持(如 Next.js) | 在服务端根据 cookie 或 user-agent 判断 |
| 组件库集成 | 将逻辑封装为 React/Vue Hook |
✅ 总结
一个优秀的深色模式实现,不仅是“换个背景色”,而是对用户体验、系统集成与状态管理的综合考量。本文提供的方案:
- ✅ 无闪烁加载
- ✅ 自动适配系统偏好
- ✅ 允许用户覆盖
- ✅ 持久化选择
- ✅ 代码简洁、无依赖
只需复制粘贴,即可为你的网站增添现代感与专业度。现在就去试试吧!
💡 小提示:GitHub、Twitter、Notion 等主流产品均采用类似策略。你也可以!