next-themes是唯一推荐的无闪烁方案,它通过服务端提前注入data-theme属性,使CSS变量在首屏渲染时即生效,避免客户端JS晚于样式加载导致的FOUC。

主题闪烁(FOUC)不是 CSS 变量本身的问题,而是变量注入时机与 HTML 渲染顺序不匹配导致的。Next.js 的 SSR 流程中,document.documentElement 在服务端不可用,而客户端 JS 又晚于初始样式生效,变量就“漏了一拍”。
next-themes 是唯一推荐的无闪烁方案
它把主题状态从 localStorage / system preference 提前“猜”出来,并在服务端就注入 data-theme 属性,让 CSS 变量在首屏渲染时就生效。
- 必须用
ThemesProvider包裹整个应用,且只能放在app/layout.tsx的顶层(不能在组件内条件渲染) -
useTheme()返回的theme值在服务端是null,但 Provider 会根据defaultTheme和attribute(默认data-theme)提前写入 HTML 根节点 - 不要手动在
useEffect里读 localStorage 并 setAttribute —— 这正是闪烁的根源 - 如果你用了
next/font,确保字体变量也绑定到同一data-theme上,比如font.variable要和ThemesProvider的attribute一致
CSS 变量定义必须在 globals.css 顶层声明
Next.js 的 SSR 会提取 app/layout.tsx 中 import 的 CSS 文件并内联到 <head>,但前提是变量定义不能被包裹在媒体查询或嵌套规则里。
- 写法正确:
:root { --bg: #fff; --text: #000; }+[data-theme="dark"] { --bg: #111; --text: #eee; } - 错误写法:
@media (prefers-color-scheme: dark) { :root { ... } }—— SSR 不执行媒体查询,这部分变量永远不生效 - 不要用 SCSS/Less 的变量替代 CSS 自定义变量,它们无法被 JS 动态修改,也无法参与 SSR 主题切换
- 如果用了 Tailwind,确保
tailwind.config.js的theme.extend.colors里没覆盖掉你的--bg等变量名,否则会被 PurgeCSS 误删
避免在组件内动态 import CSS 或修改 data-theme
任何在 useEffect、onClick 或异步逻辑里执行的 document.documentElement.setAttribute 都会导致二次重绘,即“闪一下再变”。
立即学习“前端免费学习笔记(深入)”;
- 主题切换必须只通过
useTheme().setTheme()触发 —— 它内部已做防抖 + 同步 DOM 更新 - 禁止在自定义 Hook 里封装
setAttribute,哪怕加了if (typeof window !== 'undefined')也救不了闪烁 - 动态 import 的组件(如
dynamic(() => import('./DarkModeToggle')))里写的import './toggle.css'不会 SSR,其样式必然 FOUC - 如果用了 Ant Design,确认你用了
@ant-design/nextjs-registry,而不是手动在组件里useEffect注入样式
最常被忽略的一点:next-themes 的 storageKey 默认是 theme,但如果你项目里其他逻辑也写了 localStorage.setItem('theme', ...),可能造成客户端和服务端读取不一致 —— 检查浏览器 Application → Local Storage 里的值是否和你预期完全一致,差一个空格都会导致 SSR 时 fallback 到 defaultTheme。


















