# 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.

**Type:** Playbook  
**Tools:** Cursor, Claude Code, Codex, Windsurf  
**Stack:** Next.js, TypeScript, Tailwind  
**Difficulty:** easy  
**Updated:** 2026-06-08

---

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

```txt title="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

```txt
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

```bash
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-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.
- **`dark:`-Klassen werden nicht angewendet** — `tailwind.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

```txt title="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

```md title="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
```