Next.js 13.4.10不依赖next-theme实现暗色模式的最优方案咨询
问题解答
1. 将脚本放在body内的方案是否合理?
完全合理。Tailwind文档建议把脚本放head里,核心是要让脚本在页面内容渲染前执行,尽早设置主题类避免FOUC。但Next.js 13.4.10的App Router对head中的脚本有严格限制,直接加script标签会报错,因此把内联脚本放在body最顶部是可行的替代方案——只要脚本在DOM内容渲染前执行,就能达到避免FOUC的效果,和Tailwind方案的核心逻辑一致。
2. 最优实现方式
结合服务器端Cookie读取+客户端内联脚本+客户端切换组件的方案,能彻底消除FOUC,同时满足「优先本地存储/Cookie,否则跟随系统主题」的需求,具体步骤如下:
步骤1:配置Tailwind CSS
在tailwind.config.js中设置darkMode: 'class',让Tailwind通过html标签的class识别主题:
/** @type {import('tailwindcss').Config} */ module.exports = { darkMode: 'class', content: [ './app/**/*.{js,ts,jsx,tsx,mdx}', './pages/**/*.{js,ts,jsx,tsx,mdx}', './components/**/*.{js,ts,jsx,tsx,mdx}', ], // 其他配置... }
步骤2:根布局中添加服务器端逻辑+内联脚本
在app/layout.tsx(服务器组件)中,从请求Cookie读取已保存的主题,提前给html标签设置对应class;同时在body最顶部添加内联脚本,处理本地存储和系统主题的逻辑,确保客户端渲染时主题一致:
import { headers } from 'next/headers'; export default function RootLayout({ children, }: { children: React.ReactNode }) { // 服务器端读取Cookie中的主题 const headerList = headers(); const cookie = headerList.get('cookie'); let initialTheme: 'light' | 'dark' | undefined; if (cookie) { const themeCookie = cookie.split('; ').find(row => row.startsWith('theme='))?.split('=')[1]; if (themeCookie === 'light' || themeCookie === 'dark') { initialTheme = themeCookie; } } return ( <html lang="zh-CN" className={initialTheme === 'dark' ? 'dark' : ''}> <body> {/* 内联脚本放在body最顶部,优先执行 */} <script dangerouslySetInnerHTML={{ __html: ` (function() { // 优先级:localStorage > Cookie > 系统主题 const savedTheme = localStorage.getItem('theme'); const cookieTheme = document.cookie.split('; ').find(row => row.startsWith('theme='))?.split('=')[1]; const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; const theme = savedTheme || cookieTheme || systemTheme; // 更新html标签的主题类 if (theme === 'dark') { document.documentElement.classList.add('dark'); } else { document.documentElement.classList.remove('dark'); } })(); ` }} /> {children} </body> </html> ); }
步骤3:实现客户端主题切换组件
创建客户端组件(标记'use client'),处理主题切换逻辑,同步更新localStorage、Cookie和html标签的class,同时可选监听系统主题变化:
'use client'; import { useEffect, useState } from 'react'; export default function ThemeToggle() { const [theme, setTheme] = useState<'light' | 'dark'>('light'); useEffect(() => { // 初始化当前主题 const currentTheme = document.documentElement.classList.contains('dark') ? 'dark' : 'light'; setTheme(currentTheme); // 监听系统主题变化(用户未手动设置时跟随系统) const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); const handleSystemChange = (e: MediaQueryListEvent) => { if (!localStorage.getItem('theme')) { const newTheme = e.matches ? 'dark' : 'light'; setTheme(newTheme); document.documentElement.classList.toggle('dark', newTheme === 'dark'); document.cookie = `theme=${newTheme}; path=/; max-age=31536000`; } }; mediaQuery.addEventListener('change', handleSystemChange); return () => mediaQuery.removeEventListener('change', handleSystemChange); }, []); const toggleTheme = () => { const newTheme = theme === 'light' ? 'dark' : 'light'; setTheme(newTheme); // 更新html主题类 document.documentElement.classList.toggle('dark', newTheme === 'dark'); // 保存到localStorage localStorage.setItem('theme', newTheme); // 保存到Cookie(同步服务器端逻辑) document.cookie = `theme=${newTheme}; path=/; max-age=31536000`; }; return ( <button onClick={toggleTheme} className="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800"> {theme === 'light' ? '🌙 深色模式' : '☀️ 浅色模式'} </button> ); }
方案优势
- 彻底消除FOUC:服务器端提前设置主题class,客户端脚本补充完善逻辑,确保页面渲染时主题已生效
- 符合优先级规则:严格遵循「本地存储 > Cookie > 系统主题」的逻辑
- 适配Next.js App Router:区分服务器组件和客户端组件,符合框架规则
- 良好的用户体验:支持手动切换、系统主题同步,数据持久化
内容的提问来源于stack exchange,提问作者Enes
相关产品推荐
相关产品推荐

