P PasteCode
Playbook

Prompt-to-PR : Ajouter le mode sombre

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.

CursorClaude CodeCodexWindsurf Next.jsTypeScriptTailwind
.md .json Difficulté: Facile Mis à jour 8 juin 2026

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.

1. Exigence

Ajouter une bascule persistante mode sombre/clair/système à un site Next.js 15 App Router utilisant Tailwind. Le thème doit être :

  • Sans scintillement au chargement initial (pas de flash blanc en mode sombre).
  • Persisté dans localStorage.
  • Par défaut, la préférence du système (system).
  • Bascule via un bouton dans l’en-tête du site.

2. Premier 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. Modifications de fichiers attendues

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. Liste de vérification

  • tailwind.config.ts a darkMode: "class" — pas "media" (media ne permet pas de bascule).
  • ThemeProvider est un composant client ("use client") enveloppant children du layout racine du composant serveur.
  • La racine <html> ou <body> n’a PAS de class="dark" codée en dur — next-themes ajoute cela dynamiquement.
  • ThemeToggle rend null (ou un espace réservé) jusqu’à ce que mounted === true pour éviter une incohérence d’hydratation.
  • Aucun suppressHydrationWarning sur <html> manquant — next-themes le nécessite ; confirmez qu’il est présent sur la balise <html>.
  • Les variantes dark: n’apparaissent que sur les conteneurs de niveau layout, pas sur chaque élément.
  • 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.

5. Commandes de test

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. Échecs courants

  • 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.
  • Erreur d’incohérence d’hydratationThemeToggle 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.
  • Les classes dark: ne sont pas appliquéestailwind.config.ts a toujours darkMode: "media" ou la clé est manquante. La stratégie de classe nécessite la valeur "class".
  • Bascule non visible<ThemeToggle> importé dans un composant serveur sans limite "use client". La chaîne d’import doit passer par un composant client.

7. Prompt de correction

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. Description de la PR

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