Prompt-to-PR: 添加深色模式
端到端标准操作流程,使用 next-themes 在 Next.js Tailwind 应用中实现无闪烁深色模式——采用 class 策略、系统偏好和持久化切换。
CursorClaude CodeCodexWindsurf Next.jsTypeScriptTailwind
深色模式实现不当会导致每次页面加载时出现错误的主题闪烁(FOIT)。本操作手册将使用 next-themes 配合 Tailwind 的 class 策略,产生无闪烁实现。
1. 需求
为使用 Tailwind 的 Next.js 15 App Router 站点添加持久化深色/浅色/系统模式切换。主题必须:
- 初始加载无闪烁(深色模式下无白色闪屏)。
- 持久化到
localStorage。 - 默认跟随操作系统偏好(
system)。 - 通过站点标题中的按钮进行切换。
2. 第一个提示
Add flicker-free dark mode to this Next.js 15 App Router project with Tailwind.
Requirements:1. Install `next-themes`.2. In `tailwind.config.ts`, set `darkMode: "class"`.3. Create `src/components/ThemeProvider.tsx` — a thin Client Component wrapper around `next-themes` ThemeProvider: <ThemeProvider attribute="class" defaultTheme="system" enableSystem> {children} </ThemeProvider> Mark it "use client".4. Wrap the root layout's `<body>` with ThemeProvider in `src/app/layout.tsx`. Keep the body tag; just wrap children.5. Create `src/components/ThemeToggle.tsx` — a Client Component with a button that cycles: light -> dark -> system. Use `useTheme()` from next-themes. Show the current theme as an icon or label. Handle the mounted check to avoid hydration mismatch (render null or a skeleton until mounted).6. Add `<ThemeToggle />` to the existing site header/nav component.7. Do not change any color values or CSS — only add `dark:` Tailwind variants where the existing design clearly has background and text colors that need inversion. Update: bg-white -> bg-white dark:bg-gray-950, text-gray-900 -> text-gray-900 dark:text-gray-100. Do not invert every element — only top-level layout containers.3. 预期文件变更
package.json (next-themes)tailwind.config.ts (darkMode: "class")src/app/layout.tsx (ThemeProvider wrapper)src/components/ThemeProvider.tsx (new — "use client" wrapper)src/components/ThemeToggle.tsx (new — theme cycling button)src/components/Header.tsx (add ThemeToggle — or whichever nav file)4. 审查清单
tailwind.config.ts包含darkMode: "class"— 而不是"media"(media 不允许切换)。ThemeProvider是客户端组件("use client"),包裹来自服务端组件根布局的children。- 根
<html>或<body>没有硬编码的class="dark"—next-themes会动态添加此属性。 ThemeToggle在mounted === true之前渲染null(或占位符),以防止水合不匹配。<html>上没有缺失suppressHydrationWarning—next-themes要求此属性;确认它出现在<html>标签上。dark:变体仅出现在布局级容器上,而不是每个单独的元素。next-themes使用的localStorage键是默认的theme— 没有与其他存储冲突的自定义键。
5. 测试命令
bun dev
# In Chrome DevTools: set prefers-color-scheme to dark# Reload — confirm no white flash before dark theme applies
# Toggle the button — confirm light/dark/system cycle# Reload after each — confirm preference persists
bun tsc --noEmitbun run build# Confirm no hydration warnings in the browser console after bun run start6. 常见失败
- 加载时白色闪烁(FOIT) —
ThemeProvider未包裹根布局,或者<html>缺少suppressHydrationWarning。next-themes会在<head>中注入脚本以在绘制前设置 class — 这仅在包裹最外层元素时有效。 - 水合不匹配错误 —
ThemeToggle在mounted之前渲染了主题特定内容(图标)。添加const [mounted, setMounted] = useState(false)和useEffect(() => setMounted(true), [])。在挂载前返回占位符。 dark:类未应用 —tailwind.config.ts仍然是darkMode: "media"或缺少该键。class 策略需要"class"值。- 切换不可见 —
<ThemeToggle>在没有"use client"边界的服务端组件中导入。导入链必须经过客户端组件。
7. 修复提示
There is a white flash before dark mode applies on page load.
Confirm:1. In src/app/layout.tsx, the <html> tag has suppressHydrationWarning.2. ThemeProvider wraps {children} directly inside <body>.3. ThemeProvider uses attribute="class" (not attribute="data-theme").
next-themes injects an inline script into <head> that reads localStorage andsets the class on <html> synchronously before paint. This only works if theThemeProvider is rendered above the content it styles.8. PR 描述
## Feature: Flicker-free dark mode via next-themes
- `tailwind.config.ts`: `darkMode: "class"`- `ThemeProvider` wraps root layout — `next-themes` sets `<html class="dark">` synchronously before first paint (no flash)- `ThemeToggle` cycles light / dark / system; persists to `localStorage`- Mounted guard in ThemeToggle prevents hydration mismatch- `dark:` variants added to top-level layout containers only