{
  "id": "add-dark-mode",
  "type": "playbooks",
  "category": "playbooks",
  "locale": "fr",
  "url": "/fr/playbooks/add-dark-mode",
  "title": "Prompt-to-PR : Ajouter le mode sombre",
  "description": "Procédure opérationnelle standard de bout en bout pour ajouter un mode sombre sans scintillement à une application Next.js Tailwind utilisant next-themes — stratégie de classe, préférence système et bascule persistante.",
  "tools": [
    "Cursor",
    "Claude Code",
    "Codex",
    "Windsurf"
  ],
  "stack": [
    "Next.js",
    "TypeScript",
    "Tailwind"
  ],
  "tags": [
    "nextjs",
    "tailwind",
    "typescript"
  ],
  "difficulty": "easy",
  "updated": "2026-06-08",
  "markdown": "Un mode sombre mal implémenté provoque un flash de thème incorrect (FOIT) à chaque chargement de page. Ce playbook produit une implémentation sans scintillement utilisant `next-themes` avec la stratégie `class` de Tailwind.\n\n## 1. Exigence\n\nAjouter une bascule persistante mode sombre/clair/système à un site Next.js 15 App Router utilisant Tailwind. Le thème doit être :\n- Sans scintillement au chargement initial (pas de flash blanc en mode sombre).\n- Persisté dans `localStorage`.\n- Par défaut, la préférence du système (`system`).\n- Bascule via un bouton dans l'en-tête du site.\n\n## 2. Premier 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. Modifications de fichiers attendues\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. Liste de vérification\n\n- `tailwind.config.ts` a `darkMode: \"class\"` — pas `\"media\"` (media ne permet pas de bascule).\n- `ThemeProvider` est un composant client (`\"use client\"`) enveloppant `children` du layout racine du composant serveur.\n- La racine `<html>` ou `<body>` n'a PAS de `class=\"dark\"` codée en dur — `next-themes` ajoute cela dynamiquement.\n- `ThemeToggle` rend `null` (ou un espace réservé) jusqu'à ce que `mounted === true` pour éviter une incohérence d'hydratation.\n- Aucun `suppressHydrationWarning` sur `<html>` manquant — `next-themes` le nécessite ; confirmez qu'il est présent sur la balise `<html>`.\n- Les variantes `dark:` n'apparaissent que sur les conteneurs de niveau layout, pas sur chaque élément.\n- La clé `localStorage` utilisée par `next-themes` est la valeur par défaut `theme` — pas de clé personnalisée qui entre en conflit avec d'autres stockages.\n\n## 5. Commandes de test\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. Échecs courants\n\n- **Flash blanc au chargement (FOIT)** — `ThemeProvider` n'enveloppe pas le layout racine, ou `suppressHydrationWarning` est manquant sur `<html>`. `next-themes` injecte un script dans `<head>` pour définir la classe avant le rendu — cela ne fonctionne que s'il enveloppe l'élément le plus externe.\n- **Erreur d'incohérence d'hydratation** — `ThemeToggle` rend un contenu spécifique au thème (icône) avant `mounted`. Ajoutez `const [mounted, setMounted] = useState(false)` et `useEffect(() => setMounted(true), [])`. Renvoyez un espace réservé jusqu'à ce que mounted soit true.\n- **Les classes `dark:` ne sont pas appliquées** — `tailwind.config.ts` a toujours `darkMode: \"media\"` ou la clé est manquante. La stratégie de classe nécessite la valeur `\"class\"`.\n- **Bascule non visible** — `<ThemeToggle>` importé dans un composant serveur sans limite `\"use client\"`. La chaîne d'import doit passer par un composant client.\n\n## 7. Prompt de correction\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. Description de la PR\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```"
}