必须配置 darkMode: 'class' 并操作 document.documentElement,配合内联初始化脚本,否则暗色模式不生效、刷新丢失、SSR 水合错乱;漏配或误配 darkMode 会导致 dark: 类被完全剔除。

必须用 darkMode: 'class' 配置 + 操作 document.documentElement + 内联初始化脚本,否则暗色样式不生效、刷新丢失、SSR 下水合错乱。
tailwind.config.js 里漏写 darkMode 就等于没开暗黑模式
Tailwind v3+ 默认不启用 dark: 变体。哪怕你写了 dark:bg-gray-800,只要配置里没这行:
darkMode: 'class'
编译器就直接剔除所有 dark: 类——不是失效,是压根没生成,连警告都没有。
- 非法值如
darkMode: true、darkMode: 'dark'、darkMode: ''全部等同于未配置 -
darkMode必须是顶层字段,不能塞进theme.extend或plugins - 用 TypeScript 写配置时,确保导出类型正确(比如
defineConfig),否则 Vite/Next.js 可能读不到
切换必须操作 document.documentElement,不是 body
dark: 变体只检查 元素上是否有 dark 类。改 document.body.classList 是常见错误,尤其在 Next.js/Nuxt 等 SSR 框架中会导致首屏闪白、hydration mismatch 甚至报错。
立即学习“前端免费学习笔记(深入)”;
- 正确写法:
document.documentElement.classList.toggle('dark') - 错误写法:
document.body.classList.toggle('dark')(即使视觉暂时正常,SSR 场景下必然出问题) - 别在组件内给局部容器加
dark类“期望主题透传”——dark:不继承,只看最近带dark的祖先(通常是html)
防闪白的关键:内联脚本要在 CSS 加载前执行
浏览器先渲染无 dark 类的浅色样式,JS 后加载再加类,就会“闪白”。解决办法只有一个:把初始化逻辑写成立即执行函数,放在 内的 <script></script> 标签里,且不能包 DOMContentLoaded 或 window.onload。
最小可行内联脚本(可直接贴进 HTML 的 ):
(() => {
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 {}
})()
- 必须包
try/catch:服务端渲染时window不存在,不捕获会中断 JS 执行 - 状态逻辑要覆盖三种情况:强制亮色(
'light')、强制暗色('dark')、跟随系统(null或其他值) - 别依赖
matchMedia单一判断——用户手动选了“亮色”,但系统设为深色,此时应以 localStorage 为准
dark: 样式写法容易踩坑的几个点
加了 dark 类不等于自动变暗。很多颜色在暗色下依然不可读,比如 text-gray-700 在深灰背景上几乎隐形。
- 别混用
dark:bg-gray-900和bg-white在同一元素:CSS 特异性竞争导致行为难预测 - 推荐写法:
bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100,明暗分离,清晰可控 - 组合变体时,
dark:hover:bg-gray-700比hover:dark:bg-gray-700更符合直觉,也更易维护 - 自定义颜色变量(如
theme.extend.colors.dark)比硬写gray-800更利于全局换肤
最常被忽略的是:暗色模式不是加个 dark 类就结束,而是整套语义化颜色体系要重定义——按钮、表单边框、focus ring、阴影深度,全得重新校准对比度和层级感。



















