Implémenter le mode sombre : guide complet pour les développeurs
Le mode sombre est passé d'une préférence de niche à une fonctionnalité attendue. Les utilisateurs de toutes les plateformes — macOS, Windows, Android, iOS — peuvent définir une apparence sombre à l'échelle du système, et ils s'attendent à ce que les sites web et les applications le respectent. Implémenter correctement le mode sombre demande plus que d'échanger le blanc et le noir : cela exige une approche systématique des couleurs, du contraste et du contrôle utilisateur. Ce guide parcourt le processus complet, de l'architecture CSS aux mécanismes de bascule JavaScript, jusqu'aux tests approfondis des deux thèmes.
Propriétés personnalisées CSS pour les thèmes
La façon la plus maintenable de gérer le mode sombre en CSS est d'utiliser des propriétés personnalisées (également appelées variables CSS). Au lieu de disperser des valeurs de couleur dans vos feuilles de style, vous définissez chaque couleur comme une variable sur :root et redéfinissez ensuite ces variables pour le mode sombre. Les styles de composants ne font référence qu'aux variables — jamais à des codes hexadécimaux bruts.
Définir vos palettes clair et sombre
Commencez par une palette en mode clair comme valeur par défaut. Un bon point de départ pourrait ressembler à ceci :
:root {
/* Fonds */
--color-bg-base: #FFFFFF;
--color-bg-elevated: #F8F9FA;
--color-bg-overlay: #F1F3F5;
/* Texte */
--color-text-primary: #1A1A2E;
--color-text-secondary: #4A4A6A;
--color-text-muted: #6C757D;
/* Bordures */
--color-border: #DEE2E6;
--color-border-strong: #ADB5BD;
/* Marque / accent */
--color-accent: #3B82F6;
--color-accent-hover: #2563EB;
/* Retour d'état */
--color-success: #22C55E;
--color-warning: #F59E0B;
--color-danger: #EF4444;
}
Définissez ensuite les remplacements pour le mode sombre dans un bloc séparé. L'idée clé est que vous ne vous contentez pas d'inverter les couleurs — vous choisissez une palette différente, spécifiquement conçue pour une surface sombre :
[data-theme="dark"] {
/* Fonds */
--color-bg-base: #0F0F17;
--color-bg-elevated: #1A1A2E;
--color-bg-overlay: #252540;
/* Texte */
--color-text-primary: #E8E8F0;
--color-text-secondary: #A8A8C0;
--color-text-muted: #6A6A88;
/* Bordures */
--color-border: #2E2E4A;
--color-border-strong: #4A4A6A;
/* Marque / accent — souvent légèrement plus clair pour la lisibilité sur fond sombre */
--color-accent: #60A5FA;
--color-accent-hover: #93C5FD;
/* Retour d'état — légèrement désaturé pour éviter la dureté */
--color-success: #4ADE80;
--color-warning: #FCD34D;
--color-danger: #F87171;
}
Remarquez que l'accent #3B82F6 en mode clair devient #60A5FA en mode sombre. La teinte est identique mais la luminosité augmente — cela est nécessaire car le contexte de contraste s'est inversé. Une couleur qui passe WCAG AA sur fond blanc échouera presque toujours sur un fond quasi-noir sans ajustement. Le Générateur de nuances vous permet d'explorer la gamme complète 50-950 de n'importe quelle couleur, facilitant le choix de la nuance appropriée pour chaque thème.
Utiliser les variables dans les composants
Une fois la palette établie, chaque composant fait référence aux variables plutôt qu'aux valeurs brutes :
.card {
background-color: var(--color-bg-elevated);
border: 1px solid var(--color-border);
color: var(--color-text-primary);
}
.btn-primary {
background-color: var(--color-accent);
color: #FFFFFF;
}
.btn-primary:hover {
background-color: var(--color-accent-hover);
}
Lorsque l'attribut [data-theme="dark"] est présent sur l'élément <html>, toutes les variables se mettent à jour simultanément, et chaque composant qui y fait référence change d'apparence — aucun CSS supplémentaire requis.
La media query prefers-color-scheme
Avant que l'utilisateur n'interagisse avec une bascule, vous pouvez honorer sa préférence système à l'aide de la media query prefers-color-scheme. Cette media query se déclenche lorsque le système d'exploitation est réglé sur l'apparence sombre.
@media (prefers-color-scheme: dark) {
:root {
--color-bg-base: #0F0F17;
--color-bg-elevated: #1A1A2E;
--color-bg-overlay: #252540;
--color-text-primary: #E8E8F0;
--color-text-secondary: #A8A8C0;
--color-text-muted: #6A6A88;
--color-border: #2E2E4A;
--color-border-strong: #4A4A6A;
--color-accent: #60A5FA;
--color-accent-hover: #93C5FD;
--color-success: #4ADE80;
--color-warning: #FCD34D;
--color-danger: #F87171;
}
}
Cette approche fonctionne sans JavaScript, n'entraîne aucun décalage de mise en page et respecte immédiatement la préférence déclarée de l'utilisateur au chargement de la page. C'est la bonne base de référence. La limite est que les utilisateurs ne peuvent pas l'outrepasser dans votre application — si le système d'exploitation est sombre, le site est sombre, sans échappatoire. C'est pourquoi la plupart des implémentations en production superposent une bascule JavaScript par-dessus la media query.
Combiner les deux approches
Le schéma recommandé utilise la media query comme valeur par défaut et l'attribut data-theme comme remplacement explicite. Vous pouvez gérer cela avec une astuce de spécificité CSS ou en ordonnant correctement vos règles :
/* 1. Mode clair par défaut */
:root {
--color-bg-base: #FFFFFF;
/* ... */
}
/* 2. Remplacement mode sombre OS (quand aucune préférence explicite n'est définie) */
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--color-bg-base: #0F0F17;
/* ... */
}
}
/* 3. Mode sombre explicite (bascule utilisateur via JS) */
[data-theme="dark"] {
--color-bg-base: #0F0F17;
/* ... */
}
Le sélecteur :not([data-theme="light"]) dans la media query signifie que la préférence sombre du système d'exploitation ne s'applique que si l'utilisateur n'a pas explicitement choisi le mode clair. Une fois qu'il a basculé, son choix explicite l'emporte.
Mécanisme de bascule avec JavaScript
Une bascule bien implémentée fait trois choses : elle modifie immédiatement l'apparence actuelle, elle persiste la préférence dans localStorage, et elle lit la préférence sauvegardée au chargement de la page avant le premier rendu.
Lecture de la préférence au chargement
Ce script doit s'exécuter dans <head> — avant le rendu de la page — pour éviter un flash du mauvais thème :
<head>
<script>
(function() {
const stored = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const theme = stored ?? (prefersDark ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
})();
</script>
</head>
Cela définit immédiatement data-theme sur <html> avant l'application des styles. Le navigateur calcule les valeurs correctes des propriétés personnalisées dès le premier rendu — sans flash.
La fonction de bascule
function toggleTheme() {
const current = document.documentElement.getAttribute('data-theme');
const next = current === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('theme', next);
}
// Lier à un bouton
document.getElementById('theme-toggle').addEventListener('click', toggleTheme);
Synchroniser l'état du bouton de bascule
Le bouton de bascule doit refléter visuellement le mode actuel. Une approche simple utilise des icônes :
<button id="theme-toggle" aria-label="Basculer le mode sombre">
<span class="icon-light">☀️</span>
<span class="icon-dark">🌙</span>
</button>
[data-theme="dark"] .icon-light { display: none; }
[data-theme="dark"] .icon-dark { display: inline; }
[data-theme="light"] .icon-light { display: inline; }
[data-theme="light"] .icon-dark { display: none; }
Comme la visibilité des icônes est contrôlée par des variables CSS liées à data-theme, l'état du bouton se met à jour automatiquement chaque fois que l'attribut change — aucun JavaScript supplémentaire requis.
Stratégies d'adaptation des couleurs
Choisir les couleurs du mode sombre n'est pas aussi simple que d'inverser votre palette claire. Plusieurs principes guident de bons choix de couleurs sombres.
Réduire le contraste, pas simplement l'inverser
Le texte blanc pur sur fond noir pur (#FFFFFF sur #000000) est techniquement le contraste maximum — 21:1 — mais il est cognitivement fatigant pour une lecture prolongée. Réduisez les deux extrêmes : utilisez un blanc cassé comme #E8E8F0 pour le corps du texte et un bleu marine très sombre comme #0F0F17 pour le fond de page. Cela préserve un contraste ample (toujours supérieur à 15:1) tout en réduisant la fatigue visuelle.
Utilisez le Vérificateur de contraste pour vous assurer que chaque combinaison texte/fond de votre thème sombre respecte au moins WCAG AA (4,5:1 pour le texte normal, 3:1 pour le grand texte). Les points de défaillance courants comprennent :
- Le texte d'espace réservé dans les champs de formulaire
- Les étiquettes des boutons désactivés
- Le texte de métadonnées secondaires (horodatages, signatures)
- Les boutons icône uniquement sans étiquette visible
Élévation en couches avec des surfaces sombres
En mode clair, l'élévation est généralement exprimée par des ombres portées. En mode sombre, les ombres deviennent invisibles sur les fonds sombres. La spécification Material Design 3 a introduit une approche plus efficace : les surfaces plus claires semblent plus hautes. Utilisez des fonds légèrement plus clairs pour les composants élevés :
/* Échelle d'élévation en mode sombre */
--color-bg-base: #0F0F17; /* Fond de page */
--color-bg-elevated: #1A1A2E; /* Cartes, barres latérales */
--color-bg-overlay: #252540; /* Modals, menus déroulants */
--color-bg-tooltip: #2E2E4A; /* Info-bulles */
#0F0F17 en base, #1A1A2E pour les cartes, #252540 pour les modals — chaque niveau est environ 8 à 10% plus clair en luminosité TSL. Cela crée une hiérarchie visuelle claire sans s'appuyer sur les ombres.
Désaturer légèrement les couleurs du mode sombre
Les couleurs très saturées paraissent dures et fluorescentes sur les fonds sombres. Lorsque vous adaptez vos couleurs de marque au mode sombre, réduisez la saturation de 10 à 20% tout en augmentant la luminosité. Au lieu d'un vert de succès vif #22C55E, préférez #4ADE80 — plus clair et légèrement moins saturé, qui se lit comme du succès sans agresser l'œil.
Le Générateur de nuances est idéal ici : entrez le vert ou le bleu primaire de votre marque et explorez la gamme 300-400 pour les utilisations en texte et icône en mode sombre, par opposition à la gamme 500-600 pour les éléments interactifs.
Images et médias
Les images à fond blanc paraissent incongrues en mode sombre. CSS peut aider :
/* Réduire la dureté des images en mode sombre */
[data-theme="dark"] img:not([src*=".svg"]) {
filter: brightness(0.9) contrast(1.05);
}
/* Ou permettre aux images de se fondre légèrement avec le fond */
[data-theme="dark"] img {
mix-blend-mode: luminosity;
opacity: 0.9;
}
Pour les icônes SVG qui doivent s'adapter, utiliser currentColor comme valeur de remplissage signifie qu'elles adoptent automatiquement la couleur de texte courante :
.icon { color: var(--color-text-secondary); }
<svg fill="currentColor" viewBox="0 0 24 24">...</svg>
Tester les deux modes
Des tests approfondis empêchent les régressions du mode sombre de s'introduire en production.
Émulation via les DevTools du navigateur
Chrome et Firefox proposent tous deux l'émulation du mode sombre dans les DevTools sans modifier votre paramètre système. Dans Chrome : ouvrez les DevTools, cliquez sur le menu à trois points, allez dans Plus d'outils → Rendu, et réglez « Émuler la fonctionnalité media CSS prefers-color-scheme » sur « dark ». Cela vous permet de comparer les deux modes côte à côte.
Test de contraste automatisé
Les vérifications manuelles ponctuelles sont sujettes aux erreurs. Intégrez des audits de contraste automatisés dans votre flux de développement. Utilisez des outils comme Axe ou Lighthouse en CI pour détecter les nouvelles couleurs qui ne respectent pas les seuils WCAG. Le Vérificateur de contraste vous permet de vérifier rapidement une paire premier plan/arrière-plan par rapport à tous les niveaux WCAG — collez n'importe quelle paire hexadécimale et voyez le ratio instantanément.
Tester avec du contenu réel
Les bugs du mode sombre apparaissent souvent sur des pages avec du contenu dynamique : images téléchargées par les utilisateurs, intégrations tierces, graphiques et cartes. Testez avec un échantillon de contenu réaliste, pas seulement avec la bibliothèque de composants de votre système de design de façon isolée.
Tests au niveau du système d'exploitation
Après vérification via l'émulation des DevTools, testez avec votre système d'exploitation réellement réglé en mode sombre. La media query prefers-color-scheme se déclenche en fonction du réglage du système d'exploitation, et certains navigateurs se comportent légèrement différemment selon que le réglage est réel ou émulé. Testez également la transition : changez de mode pendant qu'une page est ouverte et confirmez qu'il n'y a pas de décalages de mise en page ni d'artefacts de rendu.
Liste de contrôle des erreurs courantes
- Valeurs hexadécimales codées en dur dans le CSS des composants au lieu de variables — recherchez dans vos feuilles de style les codes hexadécimaux bruts et remplacez-les par des variables
- Icônes SVG avec
fill="#000000"codé en dur — changez enfill="currentColor" - Composants tiers qui ne respectent pas
data-theme— enveloppez-les dans une couche CSS isolée - Propriété
color-schemenon définie — ajoutezcolor-scheme: light darkà:rootpour que le chrome du navigateur (barres de défilement, contrôles de formulaire) s'adapte aussi <meta name="color-scheme">absent du<head>— ajoutez-le pour que le navigateur puisse appliquer la bonne couleur de fond avant le chargement du CSS
<meta name="color-scheme" content="light dark">
:root {
color-scheme: light dark;
}
Ce petit ajout fait basculer automatiquement les barres de défilement natives, les sélecteurs de date et autres contrôles de formulaire rendus par le système d'exploitation vers leurs variantes sombres — un détail que de nombreuses implémentations négligent.
Points clés à retenir
- Définissez toutes les couleurs comme propriétés personnalisées CSS sur
:rootet remplacez-les pour le mode sombre en utilisant[data-theme="dark"]. Les styles de composants ne font référence qu'aux variables, rendant le changement de thème sans effort une fois la palette établie. - Utilisez
prefers-color-scheme: darkcomme valeur par défaut automatique pour les utilisateurs ayant réglé leur système d'exploitation sur l'apparence sombre. Superposez une bascule JavaScript avec persistance danslocalStoragepour les utilisateurs qui souhaitent outrepasser ce comportement. - Exécutez le script anti-flash dans
<head>avant le chargement du CSS pour éviter le flash du mauvais thème au premier rendu. - Les couleurs du mode sombre ne sont pas des inversions des couleurs du mode clair — réduisez le contraste extrême, utilisez des fonds plus clairs pour exprimer l'élévation, et désaturez légèrement les accents de marque pour éviter la dureté fluorescente.
- Vérifiez chaque paire texte/fond avec le Vérificateur de contraste et utilisez le Générateur de nuances pour trouver la bonne nuance de chaque couleur de marque pour les deux thèmes.
- Ajoutez
color-scheme: light darket la balise<meta>correspondante pour que les éléments d'interface natifs du navigateur (barres de défilement, champs de saisie) basculent automatiquement.