Mode sombre Tailwind : class vs data-theme, deux approches comparées

Cette ligne qui clignote à l’écran : dark:bg-gray-900. La question revient sans cesse : pour le mode sombre Tailwind, faut-il utiliser class ou data-theme ?
J’ai passé pas mal de temps sur ces deux approches. Chaque recherche dans la doc ne donnait que des fragments ; l’ensemble restait flou. Finalement, j’ai parcouru la doc officielle, les discussions GitHub et le code de plusieurs bibliothèques populaires. Cet article rassemble les pièges rencontrés et les arbitrages que j’en ai tirés.
Les trois stratégies de mode sombre Tailwind
Commençons par un point : Tailwind propose trois stratégies, pas deux.
Stratégie media : suivre le système automatiquement
La stratégie media est le réglage par défaut de Tailwind — beaucoup l’ignorent. Elle s’appuie sur la requête média CSS prefers-color-scheme pour détecter la préférence système.
<!-- Aucune config requise, réponse automatique au système -->
<div class="bg-white dark:bg-gray-900">
Le contenu bascule selon les réglages système
</div>
Avantage évident : zéro configuration, l’utilisateur obtient un affichage cohérent avec ses habitudes. Inconvénient tout aussi net — pas de choix manuel. En environnement clair, ceux qui préfèrent le mode sombre sont mal servis.
Stratégie class : contrôle manuel
La stratégie class ajoute la classe .dark sur un élément parent (souvent <html>) pour activer le mode sombre. Le développeur reprend la main : bascule utilisateur, persistance des préférences.
<!-- Contrôle via JavaScript sur le nom de classe -->
<html class="dark">
<body class="bg-white dark:bg-gray-900">
Mode sombre actif
</body>
</html>
C’est l’approche la plus répandue. Documentation abondante, intégration fluide avec de nombreuses bibliothèques tierces.
Stratégie data-theme : sélecteur d’attribut sémantique
La stratégie data-theme utilise l’attribut data-theme="dark" plutôt qu’une classe. Plus claire sémantiquement, et naturellement extensible à plusieurs thèmes.
<html data-theme="dark">
<body class="bg-white dark:bg-gray-900">
Mode sombre actif
</body>
</html>
Étendre à d’autres thèmes est trivial — data-theme="oled" ou data-theme="sepia", à vous de définir. Très pratique quand il faut plusieurs modes d’affichage.
Stratégie class en détail
Principe de fonctionnement
Le principe est simple : lorsqu’une classe .dark est présente sur un ancêtre du DOM, tous les styles dark:* s’appliquent.
Sous Tailwind v3, on l’active dans la configuration :
// tailwind.config.js
module.exports = {
darkMode: 'class',
// ...
}
La structure du sélecteur CSS généré ressemble à ceci :
.dark .dark:bg-gray-900 {
background-color: #111827;
}
Tailwind v4 adopte une configuration CSS-first avec la directive @custom-variant :
/* global.css */
@import 'tailwindcss';
@custom-variant dark (&:where(.dark, .dark *));
Notez le pseudo-classe :where() — elle ramène la spécificité à zéro, sans perturber la cascade. Détail important.
Logique de bascule en JavaScript
Pour permettre à l’utilisateur de changer de thème, un court script suffit :
// Obtenir le thème actuel
function getTheme() {
return localStorage.getItem('theme') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
}
// Définir le thème
function setTheme(theme) {
localStorage.setItem('theme', theme);
document.documentElement.classList.toggle('dark', theme === 'dark');
}
// Initialisation
setTheme(getTheme());
Trois actions : lire la préférence dans localStorage, suivre le système si absent, basculer et sauvegarder. Suffisant pour la plupart des cas.
Éviter le flash blanc
Le flash blanc au chargement — je suis tombé dedans aussi. Avant l’exécution du JavaScript, le HTML s’affiche déjà en mode clair par défaut.
La solution : un script synchrone dans le <head>, avant le rendu du DOM :
<head>
<script>
// Exécution synchrone pour éviter le flash
if (localStorage.theme === 'dark' ||
(!('theme' in localStorage) &&
window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
</script>
</head>
Ce script doit être synchrone — ni defer ni async.
Avantages et inconvénients
Avantages :
- Implémentation simple et intuitive
- Écosystème riche, solutions matures pour chaque framework
- Bonne intégration avec next-themes et outils similaires
- Spécificité légèrement plus élevée, couverture des styles assurée
Inconvénients :
- La classe
.darkmanque de clarté sémantique — il faut réfléchir pour comprendre qu’il s’agit du mode sombre - Extension multi-thèmes via plusieurs classes, gestion plus lourde
- Adaptation supplémentaire avec les variables CSS
Stratégie data-theme en détail
Principe de fonctionnement
La stratégie data-theme repose sur un sélecteur d’attribut, pas de classe. Configuration Tailwind v4 :
@import 'tailwindcss';
@custom-variant dark (&:where([data-theme='dark'], [data-theme='dark'] *));
Sélecteur CSS généré :
[data-theme='dark'] .dark:bg-gray-900 {
background-color: #111827;
}
Tailwind v3 le supporte aussi, avec une configuration en tableau :
// tailwind.config.js
module.exports = {
darkMode: ['selector', '[data-theme="dark"]'],
}
Combinaison avec les variables CSS
Franchement, data-theme et les variables CSS forment un duo naturel. Vous pouvez définir des valeurs différentes sous chaque data-theme :
/* globals.css */
:root {
--background: 0 0% 100%;
--foreground: 222 84% 5%;
}
[data-theme='dark'] {
--background: 222 84% 5%;
--foreground: 210 40% 98%;
}
[data-theme='oled'] {
--background: 0 0% 0%; /* Noir pur */
--foreground: 0 0% 100%;
}
Puis référencer ces variables dans Tailwind :
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
}
}
}
}
En changeant l’attribut data-theme, tous les styles basés sur ces variables basculent — sans dark: sur chaque composant. Expérience bien plus confortable.
Retour d’expérience shadcn/ui
shadcn/ui adopte par défaut data-theme + variables CSS. Dans ses fichiers de styles, on trouve de nombreuses définitions de ce type :
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
/* ... autres variables */
}
.dark,
[data-theme='dark'] {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
/* ... autres variables */
}
}
Intéressant : il prend en charge à la fois .dark et [data-theme='dark'] — pour s’adapter aux habitudes. Avec shadcn/ui, l’une ou l’autre méthode convient.
Extension multi-thèmes
Le principal atout de data-theme : le multi-thèmes. Mode OLED, mode confort visuel — simple à définir :
<html data-theme="oled">
<!-- Fond noir pur, adapté aux écrans OLED -->
</html>
<html data-theme="sepia">
<!-- Fond jaune pâle, adapté à la lecture -->
</html>
La logique de bascule se réduit à modifier l’attribut :
function setTheme(theme) {
localStorage.setItem('theme', theme);
document.documentElement.dataset.theme = theme;
}
Cette flexibilité est difficile à reproduire avec la stratégie class.
Avantages et inconvénients
Avantages :
- Sémantique claire —
data-theme="dark"se comprend immédiatement - Extension multi-thèmes native
- Intégration fluide avec les variables CSS
- Compatible par défaut avec shadcn/ui, daisyUI, etc.
Inconvénients :
- Configuration selector manuelle sous Tailwind v3
- Adaptation parfois nécessaire pour certaines bibliothèques tierces
- Documentation communautaire encore moins fournie — la situation s’améliore
Matrice de comparaison
Voici un tableau récapitulatif des dimensions clés :
| Dimension | Stratégie class | Stratégie data-theme |
|---|---|---|
| Complexité d’implémentation | Faible, configuration simple | Moyenne, sélecteurs d’attribut à maîtriser |
| Clarté sémantique | Moyenne, .dark demande réflexion | Élevée, data-theme explicite |
| Extension multi-thèmes | Difficile, plusieurs classes | Facile, modifier l’attribut suffit |
| Support communautaire | Élevé, documentation riche | Moyen, en cours de généralisation |
| Intégration variables CSS | Adaptation supplémentaire | Naturellement adaptée |
| Tailwind v3 | darkMode: 'class' | darkMode: ['selector', '...'] |
| Tailwind v4 | @custom-variant | @custom-variant |
| Compatibilité bibliothèques | Vérifier au cas par cas | shadcn/ui et similaires compatibles nativement |
| Spécificité | Légèrement plus élevée (sélecteur de classe) | Identique (sélecteur d’attribut) |
Quand choisir la stratégie class ?
- Projet simple, clair et sombre uniquement
- Stack Next.js + next-themes
- Équipe familière de la config Tailwind v3
- Besoin de nombreux exemples communautaires
Quand choisir la stratégie data-theme ?
- Plusieurs thèmes (OLED, confort visuel, etc.)
- Utilisation de shadcn/ui ou bibliothèque similaire
- Intégration profonde avec les variables CSS
- Exigence élevée en sémantique
Intégration framework en pratique
Intégration Astro
L’intégration Astro + Tailwind est simple, avec un piège : les View Transitions.
Configuration de base :
// astro.config.mjs
import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
vite: {
plugins: [tailwindcss()]
}
});
Script mode sombre :
<!-- Dans le head de BaseLayout.astro -->
<script is:inline>
// Script synchrone anti-flash
const theme = localStorage.getItem('theme') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
if (theme === 'dark') {
document.documentElement.classList.add('dark');
// Ou avec data-theme
// document.documentElement.dataset.theme = 'dark';
}
</script>
Gestion des View Transitions :
Les View Transitions d’Astro re-rendent le DOM au changement de page ; l’état du mode sombre se perd facilement. Écoutez astro:after-swap pour réappliquer le thème :
<script>
document.addEventListener('astro:after-swap', () => {
const theme = localStorage.getItem('theme');
if (theme === 'dark') {
document.documentElement.classList.add('dark');
}
});
</script>
Étape cruciale — beaucoup l’oublient, moi y compris au début.
Intégration Next.js + next-themes
Pour Next.js, next-themes est recommandé. Il encapsule toute la logique de bascule, compatibilité SSR et hydratation incluses.
Installation :
npm install next-themes
Configuration du Provider :
// components/ThemeProvider.tsx
import { ThemeProvider } from 'next-themes';
export function ThemeProvider({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider
attribute="class" // Stratégie class
defaultTheme="system" // Suivre le système par défaut
enableSystem={true} // Détection système activée
disableTransitionOnChange // Éviter le flash à la bascule
>
{children}
</ThemeProvider>
);
}
Pour passer à data-theme, modifiez attribute :
<ThemeProvider attribute="data-theme" defaultTheme="system">
Utilisation dans le layout :
// app/layout.tsx
import { ThemeProvider } from './components/ThemeProvider';
export default function RootLayout({ children }) {
return (
<html lang="zh">
<body>
<ThemeProvider>
{children}
</ThemeProvider>
</body>
</html>
);
}
Composant de bascule :
// components/ThemeToggle.tsx
import { useTheme } from 'next-themes';
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
<button
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
className="p-2 rounded-lg"
>
{theme === 'dark' ? '☀️' : '🌙'}
</button>
);
}
next-themes gère localStorage, la détection système et l’hydratation. Tranquille.
Nouveautés Tailwind v4
Tailwind v4 introduit une configuration CSS-first ; le mode sombre évolue aussi.
Directive @custom-variant
Les variants autrefois définis en JavaScript se déclarent directement en CSS :
@import 'tailwindcss';
/* Stratégie class */
@custom-variant dark (&:where(.dark, .dark *));
/* Stratégie data-theme */
@custom-variant dark (&:where([data-theme='dark'], [data-theme='dark'] *));
Plus intuitif — plus besoin de reconstruire le JavaScript pour modifier la config.
Directive @theme pour les variables
Avec data-theme, définissez les variables de thème via @theme :
@import 'tailwindcss';
@custom-variant dark (&:where([data-theme='dark'], [data-theme='dark'] *));
@theme {
--color-primary: oklch(0.65 0.2 150);
--color-muted: oklch(0.9 0.02 200);
}
/* Surcharge en mode sombre */
[data-theme='dark'] {
--color-primary: oklch(0.7 0.15 180);
--color-muted: oklch(0.3 0.02 200);
}
Utilisation directe :
<button class="bg-primary text-white">Bouton</button>
Après bascule de data-theme, les couleurs suivent — sans styles redondants du type dark:bg-primary-dark.
Bascule à trois états
Pour light/dark/system, combinez avec l’API window.matchMedia :
function setTheme(theme) {
if (theme === 'system') {
localStorage.removeItem('theme');
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.dataset.theme = isDark ? 'dark' : 'light';
} else {
localStorage.setItem('theme', theme);
document.documentElement.dataset.theme = theme;
}
}
// Écouter les changements de préférence système
window.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', (e) => {
if (!localStorage.getItem('theme')) {
document.documentElement.dataset.theme = e.matches ? 'dark' : 'light';
}
});
L’utilisateur peut choisir un thème fixe ou suivre le système en permanence.
Synthèse des bonnes pratiques
Choix recommandé
Pour la plupart des projets :
- Projet simple : stratégie class + script de bascule minimal
- Avec shadcn/ui : data-theme + variables CSS directement
- Multi-thèmes requis : data-theme obligatoire
- Projet Next.js : next-themes,
attributeselon le besoin - Projet Astro : ne pas négliger les View Transitions
Astuces pratiques
Solution complète anti-flash :
<head>
<script is:inline>
// Script synchrone, avant le rendu
(function() {
const theme = localStorage.getItem('theme');
const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (theme === 'dark' || (!theme && systemDark)) {
document.documentElement.classList.add('dark');
// Ou
document.documentElement.dataset.theme = 'dark';
}
})();
</script>
</head>
Projets SSR :
Next.js et autres frameworks SSR exigent d’éviter le hydration mismatch. next-themes le gère. En implémentation manuelle :
// useEffect pour éviter le mismatch SSR
import { useEffect, useState } from 'react';
function useTheme() {
const [theme, setTheme] = useState('light');
useEffect(() => {
const saved = localStorage.getItem('theme');
setTheme(saved || 'light');
}, []);
return theme;
}
Nommage sémantique des variables CSS :
Préférez des noms sémantiques aux noms de couleur :
/* Recommandé */
:root {
--background: ...;
--foreground: ...;
--primary: ...;
--muted: ...;
}
/* Déconseillé */
:root {
--white: ...;
--black: ...;
--gray-900: ...;
}
Le nommage sémantique rend la bascule de thème plus lisible et facilite l’ajout de nouveaux thèmes.
Conclusion
En résumé : la stratégie class est simple et mature, adaptée à la majorité des projets ; data-theme est plus sémantique, mieux adaptée au multi-thèmes et à l’intégration profonde des variables CSS.
La directive @custom-variant de Tailwind v4 simplifie la configuration des deux approches. Le choix dépend de vos besoins — avec shadcn/ui, data-theme est plus naturel ; pour une bascule clair/sombre simple, class reste fiable.
Ne négligez pas les pièges d’intégration : View Transitions sous Astro, hydratation SSR sous Next.js. Mal gérés, l’expérience en souffre.
Références
- Documentation officielle Tailwind CSS Dark Mode
- Documentation Theming shadcn/ui
- next-themes sur GitHub
- Astro Dark Mode with Tailwind
FAQ
Quelle différence entre @custom-variant de Tailwind v4 et la config v3 ?
Peut-on utiliser class et data-theme en même temps ?
Trop de modificateurs dark:, le code devient verbeux — que faire ?
Méthode :
1. Définir les variables avec @theme dans globals.css
2. Les surcharger sous différents [data-theme]
3. Les référencer dans tailwind.config.js
Ainsi bg-primary s'adapte automatiquement au changement de thème.
Comment éviter la perte d'état du mode sombre dans un projet Astro ?
document.addEventListener('astro:after-swap', () => {
const theme = localStorage.getItem('theme');
if (theme === 'dark') {
document.documentElement.classList.add('dark');
}
});
Beaucoup de développeurs oublient cette étape.
Comment éviter le flash blanc au chargement ?
<script>
if (localStorage.theme === 'dark' ||
(!('theme' in localStorage) &&
window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
</script>
Le script doit être synchrone — pas defer ni async.
10 min de lecture · Publié le: 28 mars 2026 · Mis à jour le: 27 juil. 2026
Tailwind & shadcn/ui en pratique
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
Mise en page responsive avec Tailwind : container queries et stratégie de breakpoints
Plongez dans les container queries et la stratégie de breakpoints de Tailwind CSS : du viewport au conteneur, pour une mise en page responsive au niveau composant.
Partie 6 sur 14
Suivant
shadcn/ui : patterns de composition — bonnes pratiques pour faire collaborer plusieurs composants
Apprenez les meilleures pratiques des patterns de composition shadcn/ui : Dialog+Form, DataTable+DropdownMenu et scénarios courants, avec Context, gestion d'état et optimisation des performances.
Partie 8 sur 14



Commentaires
Connectez-vous avec GitHub pour laisser un commentaire