最可靠的方式是用 document.body.classlist.toggle() 或 document.documentelement.classlist.toggle() 切换主题类名,因其自动识别状态、避免覆盖已有 class、语义清晰且防错;需根据 css 主题选择器主体(body 或 html)决定操作对象,并配合 localstorage 持久化用户偏好,在 domcontentloaded 时读取并同步状态,未手动设置时才 fallback 至 matchmedia 检测系统偏好。

直接用 document.body.classList.toggle() 或 document.documentElement.classList.toggle() 切换主题类名最可靠,别碰 className 赋值或手动拼字符串。
为什么不能用 className = "dark" 直接赋值
会清空所有已有 class,比如原本有 "container js-loaded",一赋值就只剩 "dark",JS 行为、CSS 布局、第三方组件都可能崩。动态主题必须保留其他 class,只增删特定标识类。
- 用
className赋值是「覆盖写入」,不是「状态切换」 -
classList.add()/remove()也得先判断是否存在,否则重复 add 没问题但 remove 不存在的类会静默失败,逻辑难追踪 -
toggle()自动识别当前状态,一行解决「有则去,无则加」,语义清晰且防错
body 还是 html 元素上操作 class
取决于你的 CSS 主题规则怎么写的。如果写的是 body.dark .btn { ... },那就操作 body;如果写的是 :root.dark { --color-bg: #111; } 或 html.dark .card { ... },就得操作 document.documentElement(即 标签)。
- 现代方案更倾向操作
html:支持 CSS 自定义属性(:root),且避免某些浏览器对body的默认样式干扰 - 若项目已用
body.dark写了大量样式,临时切到html会导致大面积失效,不建议中途改 - 检查现有 CSS 文件里主题选择器的主体标签,再决定操作对象
保存用户偏好并自动应用主题
光切换不够,得记住用户上次选了什么,刷新页面后还得还原。用 localStorage 是最轻量可靠的方案。
- 切换时立刻存:
localStorage.setItem('theme', 'dark') - 页面加载完(推荐在
DOMContentLoaded后)读取:const saved = localStorage.getItem('theme') - 只在有值且不等于当前 class 状态时才调用
toggle(),避免无谓重绘 - 别用
cookies或服务端 session 存这个——纯前端状态,没必要走网络
document.addEventListener('DOMContentLoaded', () => {
const saved = localStorage.getItem('theme');
const root = document.documentElement;
if (saved === 'dark' && !root.classList.contains('dark')) {
root.classList.add('dark');
} else if (saved === 'light' && root.classList.contains('dark')) {
root.classList.remove('dark');
}
});
监听系统级深色模式并 fallback
用户没手动选过主题时,可以按操作系统偏好自动适配,但必须可被用户覆盖——不能「系统设深色,网站就强制深色,用户点一下亮色按钮没反应」。
- 用
window.matchMedia('(prefers-color-scheme: dark)')检测系统设置 - 只在
localStorage为空时才用它初始化主题,一旦用户手动切换,就以localStorage为准 -
matchMedia支持addEventListener,但网页生命周期内一般不需要监听变化,除非你做「跟随系统自动切换」这种非常规需求(多数用户反感)
真正容易被忽略的点是:主题切换后,prefers-color-scheme 媒体查询依然生效,如果你的 CSS 同时写了 @media (prefers-color-scheme: dark) {.btn { color: white; }} 和 .dark .btn { color: black; },两个规则会打架。要么删掉媒体查询,要么确保主题 class 的优先级更高(比如加 !important 或用更具体的选择器)。










