必须在HTML标签上操作document.documentElement且初始化脚本内联于<head>中,否则因JS执行晚于CSS解析导致首屏“闪白”;需用try/catch兼容SSR,同步更新DOM、localStorage及监听storage事件,并全面适配dark:类与HTML背景色。

必须在 HTML 标签上操作 document.documentElement,且初始化脚本要内联进 <head>,否则“闪白”无法避免——这不是 CSS 问题,是渲染时序问题。
为什么页面会“闪白”?
浏览器先解析 HTML 和 CSS,再执行 JS。如果你把主题切换逻辑放在 <body> 底部、外部 JS 文件,或用 DOMContentLoaded 触发,那么首屏一定先以浅色渲染,等 JS 执行完才加 dark 类——这个延迟就是用户看到的“闪白”。尤其在 SSR(如 Next.js)或慢网环境下更明显。
- 闪白不是样式没写对,是 DOM 类名晚于 CSS 渲染
-
darkMode: 'class'配置正确也救不了,JS 执行时机错了照样闪 - 服务端渲染时
window不存在,不包try/catch会直接报错中断
初始化脚本必须内联在 <head> 里
把这段代码直接贴进 <head> 的 <script> 标签中,不能抽成外部文件,不能加 type="module" 或 defer:
(() => {
try {
const saved = localStorage.getItem('theme')
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
const shouldDark = saved === 'dark' || (saved !== 'light' && prefersDark)
document.documentElement.classList.toggle('dark', shouldDark)
} catch {}
})()- 它会在 CSS 解析前就决定是否加
dark类,从源头掐断闪白 -
try/catch是必须的:SSR 环境下window未定义,不捕获会崩 - 别用
localStorage.theme === 'dark'直接判断——没存过时返回null,要显式处理saved !== 'light'
切换按钮逻辑必须同步更新三处
只改 DOM 或只存 localStorage 都会导致状态错乱:刷新丢设置、多标签页不同步、SSR 水合失败。
立即学习“前端免费学习笔记(深入)”;
- 点击按钮时,先调用
document.documentElement.classList.toggle('dark') - 立刻用
localStorage.setItem('theme', isDark ? 'dark' : 'light')持久化 - 监听
storage事件,确保其他标签页切换后当前页也能响应:window.addEventListener('storage', () => { /* 重新读 localStorage 并同步 classList */ })
深色模式不是加个 dark 类就结束
dark:bg-gray-800 生效了,不代表文字、边框、focus ring 就可用。Tailwind 的预设类在暗色下常不可见,比如 text-gray-700 在深灰背景上几乎隐形。
- 所有带
dark:前缀的类都要成对检查:text-gray-700 dark:text-gray-200、border-gray-200 dark:border-gray-700 - 特别注意
focus:ring:默认focus:ring-blue-500在深色背景上可能看不清,得补dark:focus:ring-blue-400 - 别依赖
darkMode: 'media'做用户可控切换——它不响应localStorage,也不支持按钮,刷新即回系统偏好
最易被忽略的是:HTML 元素本身的背景色。如果页面内容短,<html> 暴露的区域还是白色。你得显式写 bg-white dark:bg-gray-900 到 <html> 或用 @layer base 覆盖 :root 的 background-color,否则底部永远有一条白边。



















