深色模式必须用 :root 声明全局css变量并配合 .dark 类动态切换,通过 localstorage 持久化状态、服务端预设 data-theme 属性、前端初始化校准,确保首屏不闪、响应及时、全链路一致。

深色模式切换必须用 :root 声明变量,不能只在组件里定义
CSS 变量要全局生效、支持动态切换,就必须挂载在 :root 上。如果只在某个 .card 或 body 里定义 --bg-color,JS 切换类名后,子元素读不到新值——因为继承链断了。
正确做法是统一在根级声明所有主题变量,并用类名控制其值:
:root {
--bg-primary: #ffffff;
--text-primary: #333333;
--border-color: #e0e0e0;
}
<p>:root.dark {
--bg-primary: #121212;
--text-primary: #e0e0e0;
--border-color: #333333;
}</p>
- 所有主题色、间距、阴影等可变属性都走这套机制,避免重复写媒体查询或 JS 操作 style
-
:root.dark是手动加的类,不是伪类,JS 控制document.documentElement.classList.toggle('dark')即可切换 - 不要用
@media (prefers-color-scheme: dark)替代——它无法响应用户主动点击切换,且不支持服务端初始渲染时的 SSR 同构判断
JS 切换时必须检查 localStorage 并同步设置 class 和属性
用户刷新页面后深色模式丢失,90% 是因为没读取 localStorage 初始化状态。光靠 JS 绑定 click 事件远远不够。
初始化逻辑要三步到位:
const root = document.documentElement;
const savedTheme = localStorage.getItem('theme') || 'light';
<p>if (savedTheme === 'dark') {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}</p>
- 务必在 DOM 加载完成前(比如
<script></script>放里 +document.write或DOMContentLoaded前)就设置 class,否则会闪屏(白底→黑底) - 切换按钮点击后,除了 toggle class,一定要
localStorage.setItem('theme', newTheme),不然下次打开还是旧状态 - 别用
root.style.setProperty()手动设变量值——这会覆盖:root.dark的整套声明,维护成本爆炸
组件内使用变量要防 fallback 失效和命名冲突
写 color: var(--text-primary, #333) 看似稳妥,但一旦变量名拼错(比如 --text-prmiary),浏览器直接回退到 fallback,样式就崩了,还很难 debug。
- 所有变量名统一加前缀,比如
--theme-bg-primary、--theme-text-secondary,避免和第三方库(如 Tailwind 的--tw-)或未来 CSS 原生属性冲突 - 开发阶段可加一段临时 CSS 检查未定义变量:
* { color: var(--undefined-var, red) !important; },页面一红就知道漏定义了 - 不要在
@keyframes或transition中直接用变量做动画值(如background-color: var(--bg-primary)),CSS 动画不触发重绘,颜色不会渐变,得用 JS 配合matchMedia或监听 class 变化来触发重排
SSR/静态站点需在 HTML 标签上预设 class,否则首屏错乱
Next.js、Nuxt 或纯静态 HTML 页面,服务端渲染时不知道客户端偏好,:root 默认是 light,但 JS 还没执行,localStorage 也读不到——结果首屏永远亮色,哪怕用户系统设的是深色。
- Node 渲染层应根据请求头
Sec-CH-Prefers-Color-Scheme(Chrome 支持)或 UA 特征粗略判断,或干脆把class="dark"写进,再让 JS 启动后校准 - 更稳的做法:服务端不猜,统一输出
,前端 JS 第一时间读localStorage+matchMedia('(prefers-color-scheme: dark)')组合决策,然后同步设置document.documentElement.className - 注意:
documentElement在 SSR 模式下可能尚未 ready,Next.js 的useEffect或 Nuxt 的onMounted是安全时机,但别在里面反复 set class——只做一次初始化
实际最难的不是写变量,是让 class 切换、存储、服务端、动画、fallback 全部对齐。漏掉任意一环,用户就会遇到「点了切换没反应」「刷新变回白天」「动画卡顿」「暗色下按钮看不见」这类问题。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











