Mode sombre Next.js : guide complet next-themes

La première fois que j’ai ajouté un mode sombre dans un projet Next.js, ça s’est mal passé. Au chargement, un flash blanc avant le passage au sombre — des utilisateurs disaient que ça leur « brûlait les yeux ». Là j’ai compris à quel point c’était grave.
J’ai testé plusieurs approches : code maison, use-dark-mode, plein de tutos. Au final next-themes a tout changé : zéro scintillement, config simple, suivi du thème système nickel. Cet article reprend les pièges que j’ai pris et la solution qui tient la route.
Pourquoi j’ai choisi next-themes
Au début je me demandais si un petit script localStorage + class suffisait. En pratique, avec le SSR Next.js, c’est vite le bazar.
Solution maison : le flash. Le serveur ne connaît pas la préférence, rend souvent le clair, puis le client lit localStorage à l’hydratation — bascule visible.
use-dark-mode : correct, mais pas pensé pour Next.js ; des frictions en SSR.
theme-ui : très complet, trop lourd si vous voulez juste clair/sombre, bundle plus gros.
next-themes cumule 6000+ stars sur GitHub, zéro dépendance, moins de 1 ko gzippé, zéro flash, thème système et persistance auto. Le typage TypeScript est propre.
Étapes d’implémentation
Installer les dépendances
npm install next-themes
Ou avec pnpm / yarn :
pnpm add next-themes
# ou
yarn add next-themes
Créer le composant ThemeProvider
Je place souvent ce genre de provider dans providers ou components.
Fichier providers/theme-provider.tsx :
'use client'
import { ThemeProvider as NextThemesProvider } from 'next-themes'
import { type ThemeProviderProps } from 'next-themes/dist/types'
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}
Obligatoire : 'use client', car next-themes utilise les API navigateur. Sans ça, j’avais des erreurs d’hydratation partout.
Intégrer dans le layout
Avec l’App Router (Next.js 13+), dans app/layout.tsx :
import { ThemeProvider } from '@/providers/theme-provider'
import './globals.css'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="fr" suppressHydrationWarning>
<body>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</body>
</html>
)
}
Détail des options :
attribute="class" : next-themes modifie la class de <html>, idéal avec les préfixes dark: de Tailwind.
defaultTheme="system" : première visite = préférence OS.
enableSystem : indispensable pour que defaultTheme="system" fonctionne.
disableTransitionOnChange : coupe les transitions au changement de thème ; sinon tout le DOM s’anime en même temps, peu lisible. À ajuster selon le design.
suppressHydrationWarning sur <html> : next-themes peut changer la class avant l’hydratation ; sans cet attribut, React warn.
Bouton de bascule de thème
components/theme-toggle.tsx :
'use client'
import { useTheme } from 'next-themes'
import { useEffect, useState } from 'react'
export function ThemeToggle() {
const [mounted, setMounted] = useState(false)
const { theme, setTheme } = useTheme()
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) {
return null
}
return (
<button
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
className="p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
aria-label="Changer de thème"
>
{theme === 'dark' ? '🌞' : '🌙'}
</button>
)
}
Astuce : retourner null avant mounted. En SSR on n’a pas le thème ; un rendu immédiat provoque un mismatch. Après montage, useTheme est fiable.
Bascule à trois états (light / dark / system) :
export function ThemeToggle() {
const [mounted, setMounted] = useState(false)
const { theme, setTheme } = useTheme()
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) return null
const cycleTheme = () => {
if (theme === 'light') setTheme('dark')
else if (theme === 'dark') setTheme('system')
else setTheme('light')
}
const getIcon = () => {
if (theme === 'light') return '🌞'
if (theme === 'dark') return '🌙'
return '💻'
}
return (
<button
onClick={cycleTheme}
className="p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800"
>
{getIcon()}
</button>
)
}
Comprendre le scintillement
C’est ce flash qui m’a poussé à creuser le sujet.
D’où vient le FOUC
Le FOUC (Flash of Unstyled Content) est fréquent avec le mode sombre en Next.js : décalage SSR / client.
En SSR, pas de window, pas de localStorage, pas de prefers-color-scheme côté Node — le serveur rend souvent le thème clair par défaut.
Le HTML arrive, React hydrate, le JS lit localStorage, applique dark sur <html>, les styles basculent — le flash.
Comment next-themes le corrige
Un script bloquant dans <head> s’exécute avant le paint : lit localStorage, détecte le système, pose la bonne class sur <html>.
Logique simplifiée :
(function() {
try {
const theme = localStorage.getItem('theme')
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
const currentTheme = theme || systemTheme
if (currentTheme === 'dark') {
document.documentElement.classList.add('dark')
}
} catch (e) {}
})()
Script synchrone → la class est bonne avant l’affichage → pas de flash.
Erreurs de configuration courantes
Oublier suppressHydrationWarning :
Warning: Prop `className` did not match. Server: "" Client: "dark"
Ça marche souvent quand même, mais le warning pollue la console.
Mauvais emplacement de ThemeProvider :
Dans un Server Component, ou hors du body : problèmes. Il doit envelopper le contenu en Client Component.
Mauvaise config Tailwind :
module.exports = {
darkMode: 'media',
}
media suit seulement l’OS, pas la bascule manuelle. Préférez :
module.exports = {
darkMode: 'class',
}
Persistance et thème système
Persistance
Par défaut next-themes écrit dans localStorage sous la clé 'theme', sans code supplémentaire.
Clé personnalisée :
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
storageKey="my-theme"
>
{children}
</ThemeProvider>
Parfois on veut des cookies pour connaître le thème côté serveur : middleware + en-têtes + rendu SSR + sync client. Pour la plupart des apps, le défaut suffit.
Suivi du thème système
Avec enableSystem, next-themes écoute les changements OS quand le thème actif est system.
Sous le capot, prefers-color-scheme :
window.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', (e) => {
// logique de bascule
})
L’utilisateur peut forcer dark alors que l’OS est en clair : le choix est mémorisé pour les visites suivantes.
Thèmes multiples
Au-delà de clair/sombre :
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
themes={['light', 'dark', 'purple', 'green']}
>
{children}
</ThemeProvider>
CSS associé :
.purple {
--background: #f3e8ff;
--foreground: #581c87;
}
.green {
--background: #dcfce7;
--foreground: #14532d;
}
Très flexible avec des variables CSS.
Astuces et dépannage
Avec Tailwind CSS
module.exports = {
darkMode: 'class',
// autres options...
}
Puis les préfixes dark: :
<div className="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
<h1 className="text-2xl font-bold">Titre</h1>
<p className="text-gray-600 dark:text-gray-400">Paragraphe</p>
</div>
dark: s’active quand <html> a la class dark — aligné avec next-themes.
Animations et transitions
Je garde souvent disableTransitionOnChange activé : trop de transition en CSS = tout le layout qui bouge au switch.
Pour un fondu :
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange={false}
>
{children}
</ThemeProvider>
Et en global :
* {
transition: background-color 0.2s ease, color 0.2s ease;
}
À vous de voir ; sans transition c’est souvent plus net.
TypeScript
Extension des thèmes :
import { useTheme } from 'next-themes'
type Theme = 'light' | 'dark' | 'purple'
export function useCustomTheme() {
const { theme, setTheme } = useTheme()
return {
theme: theme as Theme,
setTheme: (theme: Theme) => setTheme(theme),
}
}
Autocomplétion et garde-fous sur les valeurs.
Dépannage
Le thème change mais pas les styles
darkMode: 'class'dans Tailwind ?dark:ou sélecteur.darkcorrect ?- La class sur
<html>dans les DevTools ?
Flash au rechargement
suppressHydrationWarningsur<html>?- ThemeProvider bien placé ?
- Autre script qui touche le DOM trop tôt (analytics, etc.) ?
Le thème système ne suit pas
enableSystemàtrue?- Navigateur avec
prefers-color-scheme? - Thème courant =
system(après bascule manuelle ce peut êtrelightoudark)
Synthèse
Du flash agaçant à une bascule fluide : next-themes a simplifié la vie — technique et UX.
Points clés :
next-themesrègle le flash du mode sombre Next.js avec peu de configsuppressHydrationWarningsur<html>, ThemeProvider en client- Tailwind :
darkMode: 'class' - Bouton de thème après
mounted - Système et bascule manuelle cohabitent
Pas encore testé ? Doc claire : github.com/pacocoursey/next-themes
Ajoutez un mode sombre fluide à votre projet Next.js — vos utilisateurs vous remercieront.
Implémentation complète du mode sombre Next.js
Mode sombre sans scintillement avec next-themes, suivi système et bascule manuelle
⏱️ Estimated time: 30 min
- 1
Step 1: Installer next-themes
Installation :
• npm install next-themes
Autres gestionnaires :
• pnpm add next-themes
• yarn add next-themes
Note : bibliothèque sans dépendance, très légère - 2
Step 2: Configurer ThemeProvider
Dans la mise en page racine :
• Créer providers.tsx (marqué 'use client')
• Envelopper children avec ThemeProvider
• Importer dans app/layout.tsx
Options clés :
• attribute="class" : bascule par classe
• enableSystem : suivi du thème système
• storageKey : clé localStorage
ThemeProvider doit être un composant client - 3
Step 3: Configurer Tailwind CSS
Dans tailwind.config.js :
• darkMode: 'class'
• Tailwind suit la classe sur la balise html
Exemple :
module.exports = {
darkMode: 'class',
// ... autres options
}
Styles sombres avec le préfixe dark: :
className="bg-white dark:bg-gray-900" - 4
Step 4: Corriger l'avertissement d'hydratation
Sur la balise html :
• attribut suppressHydrationWarning
• évite l'avertissement si serveur et client diffèrent
Dans layout.tsx :
<html lang="fr" suppressHydrationWarning>
<body>{children}</body>
</html> - 5
Step 5: Créer le bouton de thème
Avec le hook useTheme :
• composant ThemeToggle ('use client')
• useTheme() pour theme et setTheme
• attendre mounted avant le rendu
Exemple :
const { theme, setTheme } = useTheme()
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
if (!mounted) return null
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
Changer de thème
</button> - 6
Step 6: Tester et valider
Points de test :
• bascule manuelle sans scintillement
• suivi du thème système
• persistance après rechargement
• cohérence entre les pages
Checklist :
• chargement sans flash
• transition fluide
• localStorage correct
• suivi automatique du thème OS
FAQ
Pourquoi la page scintille au chargement ?
next-themes vs autres bibliothèques de thème ?
Comment suivre le thème système ?
Pourquoi suppressHydrationWarning ?
Pourquoi attendre mounted pour le bouton ?
Personnaliser la logique de bascule ?
Quels thèmes next-themes supporte-t-il ?
6 min de lecture · Publié le: 20 déc. 2025 · Mis à jour le: 27 juil. 2026
Guide complet Next.js
Si vous arrivez depuis la recherche, le plus rapide est de passer à l’article précédent ou suivant de cette série.
Précédent
Guide complet de surveillance Next.js en production : Sentry, logs et alertes en pratique
Guide pas à pas pour monter une surveillance Next.js en production : intégration Sentry, gestion des logs, monitoring des performances et alertes. Config App Router avec modèles de code prêts à l'emploi.
Partie 43 sur 51
Suivant
Next.js + Tailwind CSS : bonnes pratiques — guide complet de la config au mode sombre (2025)
Guide pratique Next.js + Tailwind CSS v4 (2025) : classes trop longues, thème personnalisé, mode sombre et performance. Cas réel 500 Ko → 50 Ko avec exemples de code complets.
Partie 45 sur 51



Commentaires
Connectez-vous avec GitHub pour laisser un commentaire