You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 11:25:14