{
  "id": "add-dark-mode",
  "type": "playbooks",
  "category": "playbooks",
  "locale": "de",
  "url": "/de/playbooks/add-dark-mode",
  "title": "Prompt-to-PR: Dunkelmodus hinzufügen",
  "description": "End-to-End-SOP zum Hinzufügen eines flackerfreien Dunkelmodus zu einer Next.js Tailwind-App mit next-themes — Klassenstrategie, Systemeinstellung und persistierter Umschalter.",
  "tools": [
    "Cursor",
    "Claude Code",
    "Codex",
    "Windsurf"
  ],
  "stack": [
    "Next.js",
    "TypeScript",
    "Tailwind"
  ],
  "tags": [
    "nextjs",
    "tailwind",
    "typescript"
  ],
  "difficulty": "easy",
  "updated": "2026-06-08",
  "markdown": "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.\n\n## 1. Anforderung\n\nFügen Sie einen persistenten Dunkel-/Hell-/Systemmodus-Umschalter zu einer Next.js 15 App Router-Site mit Tailwind hinzu. Das Design muss:\n- Flackerfrei beim ersten Laden (kein weißer Blitz im Dunkelmodus).\n- In `localStorage` gespeichert.\n- Standardmäßig die Betriebssystemeinstellung (`system`) verwenden.\n- Über einen Button in der Site-Header umschaltbar sein.\n\n## 2. Erster Prompt\n\n```txt title=\"First Prompt\"\nAdd flicker-free dark mode to this Next.js 15 App Router project with Tailwind.\n\nRequirements:\n1. Install `next-themes`.\n2. In `tailwind.config.ts`, set `darkMode: \"class\"`.\n3. Create `src/components/ThemeProvider.tsx` — a thin Client Component wrapper\n   around `next-themes` ThemeProvider:\n     <ThemeProvider attribute=\"class\" defaultTheme=\"system\" enableSystem>\n       {children}\n     </ThemeProvider>\n   Mark it \"use client\".\n4. Wrap the root layout's `<body>` with ThemeProvider in\n   `src/app/layout.tsx`. Keep the body tag; just wrap children.\n5. Create `src/components/ThemeToggle.tsx` — a Client Component with a button\n   that cycles: light -> dark -> system. Use `useTheme()` from next-themes.\n   Show the current theme as an icon or label. Handle the mounted check to\n   avoid hydration mismatch (render null or a skeleton until mounted).\n6. Add `<ThemeToggle />` to the existing site header/nav component.\n7. Do not change any color values or CSS — only add `dark:` Tailwind variants\n   where the existing design clearly has background and text colors that need\n   inversion. Update: bg-white -> bg-white dark:bg-gray-950, text-gray-900\n   -> text-gray-900 dark:text-gray-100.\n   Do not invert every element — only top-level layout containers.\n```\n\n## 3. Erwartete Dateiänderungen\n\n```txt\npackage.json                          (next-themes)\ntailwind.config.ts                    (darkMode: \"class\")\nsrc/app/layout.tsx                    (ThemeProvider wrapper)\nsrc/components/ThemeProvider.tsx      (new — \"use client\" wrapper)\nsrc/components/ThemeToggle.tsx        (new — theme cycling button)\nsrc/components/Header.tsx             (add ThemeToggle — or whichever nav file)\n```\n\n## 4. Überprüfungscheckliste\n\n- `tailwind.config.ts` enthält `darkMode: \"class\"` — nicht `\"media\"` (media erlaubt keinen Umschalter).\n- `ThemeProvider` ist eine Client-Komponente (`\"use client\"`), die `children` aus dem Server-Komponenten-Root-Layout umschließt.\n- Root `<html>` oder `<body>` hat KEINE hartcodierte `class=\"dark\"` — `next-themes` fügt diese dynamisch hinzu.\n- `ThemeToggle` rendert `null` (oder einen Platzhalter), bis `mounted === true` ist, um Hydrationskonflikte zu vermeiden.\n- Kein fehlendes `suppressHydrationWarning` auf `<html>` — `next-themes` erfordert es; bestätigen Sie, dass es auf dem `<html>`-Tag vorhanden ist.\n- `dark:`-Varianten erscheinen nur auf Layout-Ebene-Containern, nicht auf jedem einzelnen Element.\n- Der von `next-themes` verwendete `localStorage`-Schlüssel ist der Standard `theme` — kein benutzerdefinierter Schlüssel, der mit anderem Speicher kollidiert.\n\n## 5. Testbefehle\n\n```bash\nbun dev\n\n# In Chrome DevTools: set prefers-color-scheme to dark\n# Reload — confirm no white flash before dark theme applies\n\n# Toggle the button — confirm light/dark/system cycle\n# Reload after each — confirm preference persists\n\nbun tsc --noEmit\nbun run build\n# Confirm no hydration warnings in the browser console after bun run start\n```\n\n## 6. Häufige Fehler\n\n- **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.\n- **Hydrationskonflikt-Fehler** — `ThemeToggle` 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.\n- **`dark:`-Klassen werden nicht angewendet** — `tailwind.config.ts` hat immer noch `darkMode: \"media\"` oder der Schlüssel fehlt. Die Klassenstrategie erfordert den Wert `\"class\"`.\n- **Umschalter nicht sichtbar** — `<ThemeToggle>` wird in einer Server-Komponente ohne `\"use client\"`-Grenze importiert. Die Importkette muss durch eine Client-Komponente führen.\n\n## 7. Korrektur-Prompt\n\n```txt title=\"Fix Prompt\"\nThere is a white flash before dark mode applies on page load.\n\nConfirm:\n1. In src/app/layout.tsx, the <html> tag has suppressHydrationWarning.\n2. ThemeProvider wraps {children} directly inside <body>.\n3. ThemeProvider uses attribute=\"class\" (not attribute=\"data-theme\").\n\nnext-themes injects an inline script into <head> that reads localStorage and\nsets the class on <html> synchronously before paint. This only works if the\nThemeProvider is rendered above the content it styles.\n```\n\n## 8. PR-Beschreibung\n\n```md title=\"PR description\"\n## Feature: Flicker-free dark mode via next-themes\n\n- `tailwind.config.ts`: `darkMode: \"class\"`\n- `ThemeProvider` wraps root layout — `next-themes` sets `<html class=\"dark\">`\n  synchronously before first paint (no flash)\n- `ThemeToggle` cycles light / dark / system; persists to `localStorage`\n- Mounted guard in ThemeToggle prevents hydration mismatch\n- `dark:` variants added to top-level layout containers only\n```"
}