P PasteCode
Playbook

Prompt-to-PR: Dunkelmodus hinzufügen

End-to-End-SOP zum Hinzufügen eines flackerfreien Dunkelmodus zu einer Next.js Tailwind-App mit next-themes — Klassenstrategie, Systemeinstellung und persistierter Umschalter.

CursorClaude CodeCodexWindsurf Next.jsTypeScriptTailwind
.md .json Schwierigkeit: Einfach Aktualisiert 8. Juni 2026

Falsch implementierter Dunkelmodus verursacht bei jedem Seitenaufruf ein Aufblitzen des falschen Designs (FOIT). Dieses Playbook liefert eine flackerfreie Implementierung mit next-themes und Tailwinds class-Strategie.

1. Anforderung

Fügen Sie einen persistenten Dunkel-/Hell-/Systemmodus-Umschalter zu einer Next.js 15 App Router-Site mit Tailwind hinzu. Das Design muss:

  • Flackerfrei beim ersten Laden (kein weißer Blitz im Dunkelmodus).
  • In localStorage gespeichert.
  • Standardmäßig die Betriebssystemeinstellung (system) verwenden.
  • Über einen Button in der Site-Header umschaltbar sein.

2. Erster Prompt

First Prompt
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. Erwartete Dateiänderungen

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. Überprüfungscheckliste

  • tailwind.config.ts enthält darkMode: "class" — nicht "media" (media erlaubt keinen Umschalter).
  • ThemeProvider ist eine Client-Komponente ("use client"), die children aus dem Server-Komponenten-Root-Layout umschließt.
  • Root <html> oder <body> hat KEINE hartcodierte class="dark"next-themes fügt diese dynamisch hinzu.
  • ThemeToggle rendert null (oder einen Platzhalter), bis mounted === true ist, um Hydrationskonflikte zu vermeiden.
  • Kein fehlendes suppressHydrationWarning auf <html>next-themes erfordert es; bestätigen Sie, dass es auf dem <html>-Tag vorhanden ist.
  • dark:-Varianten erscheinen nur auf Layout-Ebene-Containern, nicht auf jedem einzelnen Element.
  • Der von next-themes verwendete localStorage-Schlüssel ist der Standard theme — kein benutzerdefinierter Schlüssel, der mit anderem Speicher kollidiert.

5. Testbefehle

Terminal window
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 --noEmit
bun run build
# Confirm no hydration warnings in the browser console after bun run start

6. Häufige Fehler

  • Weißer Blitz beim Laden (FOIT)ThemeProvider umschließt nicht das Root-Layout, oder suppressHydrationWarning fehlt auf <html>. next-themes injiziert ein Skript in <head>, um die Klasse vor dem Rendern zu setzen — das funktioniert nur, wenn es das äußerste Element umschließt.
  • Hydrationskonflikt-FehlerThemeToggle rendert designabhängige Inhalte (Symbol) vor mounted. Fügen Sie const [mounted, setMounted] = useState(false) und useEffect(() => setMounted(true), []) hinzu. Geben Sie einen Platzhalter zurück, bis mounted wahr ist.
  • dark:-Klassen werden nicht angewendettailwind.config.ts hat immer noch darkMode: "media" oder der Schlüssel fehlt. Die Klassenstrategie erfordert den Wert "class".
  • Umschalter nicht sichtbar<ThemeToggle> wird in einer Server-Komponente ohne "use client"-Grenze importiert. Die Importkette muss durch eine Client-Komponente führen.

7. Korrektur-Prompt

Fix Prompt
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 and
sets the class on <html> synchronously before paint. This only works if the
ThemeProvider is rendered above the content it styles.

8. PR-Beschreibung

PR description
## 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