data-theme 必须设在 document.documentElement 上,否则 CSS 变量和伪元素失效;正确写法为 document.documentElement.dataset.theme = 'dark' 或 'light',且需在 <head> 顶部脚本中同步 localStorage、系统偏好及 <meta name="color-scheme">。

data-theme 必须设在 document.documentElement 上
设在 body 或 wrapper div 里,CSS 变量和伪元素(比如 input::-webkit-inner-spin-button)全都不生效。原因很直接:CSS 自定义属性级联依赖根元素,:root 就是 html,不是 body。
常见错误是 JS 里写 document.body.classList.toggle('dark'),结果切了没反应——因为你的 :root 规则根本没被触发。
- 正确写法:
document.documentElement.dataset.theme = 'dark'或'light' - CSS 中必须用
html[data-theme="dark"] :root覆盖变量,不能只写html[data-theme="dark"] - 服务端渲染(SSR)场景下,这个属性还得提前注入,否则首屏必闪
localStorage 读取时机不能晚于 CSS 加载
等 DOMContentLoaded 再读 localStorage,页面已经按默认主题渲染完了,用户会看到一次“闪白”或“闪黑”。这不是体验问题,是结构缺陷。
必须把初始化脚本塞进 <head> 最顶部、所有 <link rel="stylesheet"> 之前:
立即学习“前端免费学习笔记(深入)”;
<script>
const saved = localStorage.getItem('theme');
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.dataset.theme = saved ?? (systemPrefersDark ? 'dark' : 'light');
</script>
-
??是必须的——localStorage.getItem()返回null,||会把空字符串也当 false - fallback 后必须显式赋值,留空会导致
data-theme属性不存在,整个 CSS 规则失效 - 别漏掉同步更新
<meta name="color-scheme">,否则 Safari/Chrome 地址栏、表单控件颜色不匹配
手动切换必须压过 prefers-color-scheme 媒体查询
仅靠 @media (prefers-color-scheme: dark) 实现不了用户点击切换。它只读,不写;只响应系统变更,不响应按钮点击。
真正起效的是属性选择器,不是媒体查询。媒体查询只能兜底,且要加保护:
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg-primary: #121212;
}
}
- 所有主题变量必须定义在
:root,子选择器里重定义无效 - 暗色覆盖规则优先用
html[data-theme="dark"] :root,比 class 更干净,不干扰 Tailwind/BEM 类名 - 监听系统变化要用
matchMedia(...).addEventListener('change', ...),但仅用于更新 UI 状态提示,不要自动切主题——那会覆盖用户手动选择
按钮必须用 <button> + JS,别用纯 CSS 的 :checked
用 checkbox + label + :checked 实现切换,看着省事,实际三个致命问题:无法持久化、无法响应系统主题变更、无法控制 <meta name="color-scheme">。
按钮本身还得支持可访问性:
- 必须是
<button type="button">,不是<div>或<a> - 加上
aria-pressed并随状态更新:btn.setAttribute('aria-pressed', isDark) - 点击后同步做三件事:改
dataset.theme、存localStorage、更新<meta name="color-scheme">
最易被忽略的点:所有颜色都得从 var(--text-primary) 这类变量取值。哪怕只有一处写死 #333,切换主题时那一块就卡在旧色里不动。



















