Le dark mode n'est plus un bonus — c'est un attendu. Les utilisateurs basculent entre clair et sombre selon la lumière ambiante, leurs préférences personnelles et la fatigue oculaire. Une landing page qui ignore le dark mode paraît cassée sur la moitié des écrans de vos visiteurs. Ce guide couvre les patterns et détails d'implémentation pour construire des composants React qui fonctionnent parfaitement dans les deux thèmes.
L'approche par custom properties CSS
La fondation de tout système de thème repose sur les custom properties CSS (variables). Définissez vos tokens de couleur une seule fois, puis permutez leurs valeurs selon le thème actif :
/* globals.css */
:root {
--color-background: #ffffff;
--color-foreground: #0a0a0a;
--color-card: #ffffff;
--color-border: #e5e5e5;
--color-muted: #737373;
--color-primary: #2563eb;
--color-primary-foreground: #ffffff;
}
[data-theme="dark"] {
--color-background: #0a0a0a;
--color-foreground: #fafafa;
--color-card: #171717;
--color-border: #262626;
--color-muted: #a3a3a3;
--color-primary: #3b82f6;
--color-primary-foreground: #ffffff;
}
Les composants référencent ces tokens — jamais de valeurs hex en dur :
<section
style={{
background: "var(--color-background)",
color: "var(--color-foreground)",
}}
>
<div style={{ borderColor: "var(--color-border)" }}>
{/* contenu */}
</div>
</section>
Quand l'attribut data-theme change sur l'élément racine, tous les composants se mettent à jour simultanément. Pas de prop drilling, pas de re-renders de context — la pure cascade CSS.
Dark mode avec Tailwind CSS
Tailwind fournit le préfixe de variante dark:. Dans Tailwind v4, le dark mode utilise la media query prefers-color-scheme par défaut. Pour un toggle contrôlé par l'utilisateur (recommandé), configurez le sélecteur dans votre CSS :
/* app.css — Tailwind v4 */
@import "tailwindcss";
@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
Vous pouvez maintenant utiliser les utilitaires dark: partout :
export function FeatureCard({ title, description }: { title: string; description: string }) {
return (
<div className="rounded-2xl border border-neutral-200 bg-white p-6 dark:border-neutral-800 dark:bg-neutral-900">
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
{title}
</h3>
<p className="mt-2 text-sm text-neutral-600 dark:text-neutral-400">
{description}
</p>
</div>
);
}
Le préfixe dark: est propre et co-localisé avec le markup du composant. Pas de feuilles de style séparées, pas de coût runtime CSS-in-JS.
Toggle de thème avec transition fluide
Un toggle de thème doit (1) mettre à jour l'attribut DOM, (2) persister la préférence, et (3) éviter un flash du mauvais thème au chargement. Voici une implémentation complète :
"use client";
import { useEffect, useState } from "react";
import { motion } from "motion/react";
type Theme = "light" | "dark";
function getStoredTheme(): Theme {
if (typeof window === "undefined") return "light";
return (localStorage.getItem("theme") as Theme) ?? "light";
}
export function ThemeToggle() {
const [theme, setTheme] = useState<Theme>("light");
useEffect(() => {
const stored = getStoredTheme();
setTheme(stored);
document.documentElement.setAttribute("data-theme", stored);
}, []);
function toggle() {
const next = theme === "light" ? "dark" : "light";
setTheme(next);
document.documentElement.setAttribute("data-theme", next);
localStorage.setItem("theme", next);
}
return (
<button
onClick={toggle}
className="relative h-8 w-14 rounded-full bg-neutral-200 dark:bg-neutral-700"
aria-label="Changer de thème"
>
<motion.div
className="absolute top-1 left-1 h-6 w-6 rounded-full bg-white shadow-sm"
animate={{ x: theme === "dark" ? 24 : 0 }}
transition={{ type: "spring", stiffness: 500, damping: 30 }}
/>
</button>
);
}
Pour éviter le flash du mauvais thème, ajoutez un script inline bloquant dans le <head> de votre layout racine. Ce script lit le thème stocké depuis localStorage et définit l'attribut data-theme de manière synchrone avant l'hydratation de React :
// app/layout.tsx — dans <head>
<script>{`
(function() {
var theme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', theme);
})();
`}</script>
Cela s'exécute avant le premier rendu, donc la page s'affiche avec le bon thème dès le départ.
Design dark-first
La plupart des développeurs designent en mode clair d'abord, puis ajoutent le dark mode après coup. Résultat : le dark mode paraît délavé, faible en contraste, ou a des éléments oubliés avec des fonds blancs.
Une meilleure approche : designer le dark mode en premier. Les interfaces sombres exposent les problèmes de contraste immédiatement. Si un composant est beau sur fond sombre, il fonctionnera presque certainement en mode clair avec des ajustements mineurs. L'inverse n'est pas vrai.
Règles pratiques pour le design dark-first :
- Bordures plutôt qu'ombres. Les ombres sont quasi invisibles sur fond sombre. Utilisez des bordures (
border-neutral-800) pour définir les contours des cartes en dark mode, puis ajoutez optionnellement des ombres pour le mode clair. - Fonds atténués pour la profondeur. Au lieu d'ombres, utilisez des nuances de fond légèrement plus claires pour créer de l'élévation :
bg-neutral-900pour la page,bg-neutral-800pour les cartes,bg-neutral-700pour les éléments surélevés. - Évitez le noir pur. Les fonds
#000000créent un contraste excessif avec le texte blanc, causant de la fatigue oculaire. Utilisez#0a0a0aou#111111à la place. - Testez à un minimum de 3:1 de contraste. WCAG AA exige 4.5:1 pour le texte courant et 3:1 pour les grands textes. Les palettes dark mode échouent souvent à ce test — vérifiez avec le panneau accessibilité des DevTools du navigateur.
Ratios de contraste
Le bug dark mode le plus courant est un contraste insuffisant. Voici un aide-mémoire pour le texte neutre sur fonds sombres :
| Fond | Couleur du texte | Ratio de contraste | WCAG AA |
|------|-----------------|-------------------|---------|
| #0a0a0a | #fafafa | 19.3:1 | OK |
| #0a0a0a | #a3a3a3 | 7.2:1 | OK |
| #0a0a0a | #737373 | 4.2:1 | OK (grand texte) |
| #0a0a0a | #525252 | 2.6:1 | Echec |
| #171717 | #a3a3a3 | 6.3:1 | OK |
| #171717 | #737373 | 3.7:1 | OK (grand texte) |
Utilisez #a3a3a3 ou plus clair pour le texte courant sur fonds sombres. Réservez #737373 aux labels, légendes et autres textes secondaires en grande taille uniquement.
Patterns de composants
Quand vous construisez des composants de sections compatibles dark mode, suivez ces patterns :
- Utilisez des noms de tokens sémantiques (
--color-foreground) et non des couleurs brutes (#0a0a0a). Cela rend le theming automatique. - Ne hardcodez jamais
bg-white— utilisez des tokensbg-cardoubg-backgroundqui permutent en dark mode. - Les images et illustrations ont besoin de variantes sombres ou de fonds transparents. Un PNG avec fond blanc sur une page sombre paraît cassé.
- Les blocs de code doivent utiliser un thème syntaxique qui fonctionne sur les deux fonds, ou changer de thème avec le toggle.
Des sections dark mode prêtes à l'emploi
Le catalogue Incubator propose 844+ sections React construites avec un système de tokens CSS qui supporte les modes clair et sombre nativement. Chaque section — des blocs hero aux tables de pricing en passant par les grilles de features — utilise des tokens de couleur sémantiques qui répondent instantanément aux changements de thème. Parcourez le catalogue, activez le dark mode, et copiez les sections qui correspondent à votre design.