Créer un sélecteur de thèmes avec les variables CSS
Les valeurs de couleurs codées en dur, dispersées dans une feuille de style, rendent le changement de thème pénible. Modifier une seule couleur oblige à fouiller à travers des dizaines de déclarations. Les variables CSS résolvent ce problème de façon élégante : il suffit de définir vos design tokens une seule fois, puis de basculer entre les thèmes en modifiant un unique attribut sur l’élément racine.
Cet article détaille la création d’un sélecteur de thèmes maintenable : un sélecteur qui respecte automatiquement les préférences système, réagit aux choix manuels de l’utilisateur et conserve ces choix d’une session à l’autre.
Points clés à retenir
- Les propriétés personnalisées CSS sont dynamiques et se mettent à jour à l’exécution, ce qui en fait des design tokens idéaux pour la gestion des thèmes.
- Un attribut
data-themesur l’élément racine offre une manière propre et évolutive de basculer entre un nombre illimité de thèmes. - La media query
prefers-color-schemegère les valeurs par défaut du système, tandis que JavaScript etlocalStoragepréservent les choix manuels de l’utilisateur. - Un petit script inline dans le
<head>empêche l’affichage fugace d’un thème incorrect au chargement de la page. - L’inclusion de la propriété
color-schemegarantit que l’interface native du navigateur s’adapte au thème actif.
Pourquoi les variables CSS constituent la bonne base pour la gestion des thèmes
Les propriétés personnalisées CSS sont dynamiques. Contrairement aux variables Sass qui sont compilées en valeurs statiques, les variables CSS vivent dans le navigateur et peuvent être modifiées à l’exécution — par des media queries, par JavaScript, ou par un élément parent qui change d’état.
Cela en fait des design tokens idéaux. Définissez vos couleurs de surface, vos couleurs de texte et vos couleurs d’accentuation comme variables sur :root, puis référencez ces variables dans toute votre feuille de style. Changer de thème devient alors une simple modification du DOM plutôt qu’un échange de feuilles de style.
Mettre en place vos tokens de thème
Commencez par définir votre thème clair comme thème par défaut, puis déclarez une variante sombre à l’aide d’un sélecteur d’attribut data-theme :
:root {
color-scheme: light;
--color-bg: #ffffff;
--color-text: #1a1a1a;
--color-primary: #302ae6;
--color-surface: #f4f4f4;
}
[data-theme="dark"] {
color-scheme: dark;
--color-bg: #161625;
--color-text: #e1e1ff;
--color-primary: #9a97f3;
--color-surface: #1e1e30;
}
La propriété color-scheme mérite d’être incluse. Elle indique au navigateur quel mode est actif, afin que les éléments d’interface natifs — barres de défilement, champs de formulaire, anneaux de focus — s’adaptent automatiquement à votre thème.
Utilisez maintenant ces tokens dans toute votre feuille de style :
body {
background-color: var(--color-bg);
color: var(--color-text);
}
Respecter les préférences système avec prefers-color-scheme
Les préférences système et le changement manuel de thème répondent à deux problèmes différents. prefers-color-scheme gère le cas automatique — les utilisateurs qui ont configuré leur système d’exploitation en mode sombre et s’attendent à ce que les sites web s’y conforment :
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
color-scheme: dark;
--color-bg: #161625;
--color-text: #e1e1ff;
--color-primary: #9a97f3;
--color-surface: #1e1e30;
}
}
La garde :not([data-theme="light"]) garantit que le choix explicite de l’utilisateur prévaut sur la valeur par défaut du système. Sans elle, la media query écraserait la sélection manuelle.
Une note sur light-dark()
La fonction CSS light-dark() permet de déclarer les deux valeurs de thème en ligne :
:root {
color-scheme: light dark;
--color-bg: light-dark(#ffffff, #161625);
}
Elle est élégante pour des ensembles de tokens plus simples et fonctionne bien lorsque vous n’avez besoin que de deux thèmes. Pour des systèmes comportant plus de deux thèmes ou des tokens qui varient au-delà de la couleur, l’approche par sélecteur d’attribut vous donne davantage de contrôle.
Notez que light-dark() exige que color-scheme soit défini avec les deux valeurs pour que la fonction se résolve correctement. La prise en charge dans les navigateurs modernes est solide, mais reste relativement récente (Chrome 123+, Safari 17.5+, Firefox 120+), il convient donc de confirmer la compatibilité de votre audience avant de vous y fier.
Discover how at OpenReplay.com.
Ajouter du JavaScript pour un contrôle manuel persistant
La détection des préférences système ne nécessite que du CSS, mais mémoriser le choix manuel d’un utilisateur exige du JavaScript. Voici une implémentation minimale, prête pour la production :
const STORAGE_KEY = 'theme-preference';
const root = document.documentElement;
function getSystemTheme() {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
function getSavedTheme() {
try {
return localStorage.getItem(STORAGE_KEY);
} catch {
return null;
}
}
function applyTheme(theme) {
root.setAttribute('data-theme', theme);
try {
localStorage.setItem(STORAGE_KEY, theme);
} catch {
// Storage unavailable — theme still applies for this session
}
}
// On load: respect saved preference, fall back to system
applyTheme(getSavedTheme() || getSystemTheme());
// Wire up your toggle button
document.getElementById('theme-toggle').addEventListener('click', () => {
const current = root.getAttribute('data-theme');
applyTheme(current === 'dark' ? 'light' : 'dark');
});
// Sync with OS changes when no manual preference is set
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
if (!getSavedTheme()) {
root.setAttribute('data-theme', e.matches ? 'dark' : 'light');
}
});
Les blocs try/catch entourant localStorage gèrent élégamment les modes de navigation privée et les erreurs de quota de stockage — le thème s’applique tout de même pour la session en cours, même s’il ne peut pas être sauvegardé.
Éviter le flash du mauvais thème
Si votre JavaScript se charge après le rendu de la page, les utilisateurs verront brièvement le thème par défaut avant que le bon ne s’applique. Corrigez cela avec un petit script inline bloquant dans le <head> :
<script>
try {
const saved = localStorage.getItem('theme-preference');
if (saved) {
document.documentElement.setAttribute('data-theme', saved);
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.setAttribute('data-theme', 'dark');
}
} catch {
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.setAttribute('data-theme', 'dark');
}
}
</script>
Ce script s’exécute de manière synchrone avant tout rendu de contenu, éliminant ainsi le flash. Placez-le le plus tôt possible dans le <head>, avant toute feuille de style, afin que l’attribut existe au moment où le CSS est appliqué pour la première fois.
Un pattern qui passe à l’échelle
Les variables CSS offrent une séparation nette entre vos design tokens et les styles de vos composants. Les composants référencent les variables ; les thèmes définissent les variables. Ajouter un troisième thème — contraste élevé, variante de marque, palette saisonnière — revient à ajouter un nouveau bloc de sélecteur d’attribut en CSS et une nouvelle option dans la logique de votre sélecteur. Rien d’autre ne change.
Conclusion
La véritable valeur ici ne réside pas dans le mode sombre seul — elle réside dans une architecture de gestion des thèmes qui reste maintenable à mesure que votre design system se développe. En traitant les variables CSS comme des design tokens, en respectant à la fois les préférences système et celles de l’utilisateur, et en gérant les cas limites (échecs de stockage, flashs au rendu, changements au niveau de l’OS), vous construisez un sélecteur qui paraît natif à la plateforme et trivial à étendre. Le pattern est compact, et les bénéfices s’accumulent avec chaque nouveau thème ou composant que vous ajoutez.
FAQ
Les deux fonctionnent, mais un attribut data-theme est généralement plus propre car il exprime une valeur unique plutôt qu'une liste de modificateurs. Les classes conviennent mieux à un état compositionnel où plusieurs indicateurs coexistent. Avec les thèmes, vous avez typiquement une seule valeur active à la fois, ce qui correspond plus naturellement à la sémantique d'un sélecteur d'attribut.
Ce flash se produit lorsque votre JavaScript s'exécute après que le navigateur a déjà peint la page avec les styles par défaut. La solution consiste à placer un petit script inline synchrone dans le head, qui lit la préférence enregistrée depuis localStorage et définit l'attribut data-theme avant tout rendu. Pour les visiteurs qui viennent pour la première fois, vous pouvez également vérifier prefers-color-scheme afin que le rendu initial corresponde immédiatement au thème du système.
Oui. Les variables CSS peuvent contenir n'importe quelle valeur CSS valide, y compris des échelles d'espacement, des rayons de bordure, des ombres, des piles de polices et des durées d'animation. Cela les rend utiles pour gérer la densité d'un thème (mises en page compactes ou confortables), les préférences de mouvement, ou encore les variantes typographiques. Traitez les variables comme des design tokens au sens large, et non pas uniquement comme des références de couleurs.
Elle fonctionne bien dans les navigateurs modernes (Chrome 123+, Safari 17.5+, Firefox 120+) mais n'est pas prise en charge dans les versions plus anciennes. Pour les projets ciblant un public moderne, c'est un choix élégant. Pour une compatibilité plus large, l'approche par attribut data-theme reste plus sûre et s'adapte également à plus de deux thèmes, ce que light-dark() ne peut pas faire à elle seule.
Truly understand users experience
See every user interaction, feel every frustration and track all hesitations with OpenReplay — the open-source digital experience platform. It can be self-hosted in minutes, giving you complete control over your customer data. . Check our GitHub repo and join the thousands of developers in our community..