Les thèmes sombres bricolés après coup laissent une trace reconnaissable : une variable ici, un !important là, un fichier dark.css dupliqué qui diverge du thème clair au fil des sprints. Le problème n’est pas la couleur, c’est l’absence de structure. Les design tokens en CSS custom properties apportent cette structure, sans dépendance ni couche JavaScript.
Un token n’est pas une couleur
La confusion la plus répandue consiste à assimiler une valeur brute (#1e40af) à un token. Un design token est une intention nommée : « la couleur du texte principal », et non « du bleu foncé ». D’où une organisation en deux couches. La couche primitive contient les valeurs brutes, jamais utilisées directement par les composants. La couche sémantique contient les intentions, et c’est la seule que les sélecteurs manipulent.
:root {
/* Couche 1 : primitives (jamais utilisées telles quelles) */
--gray-900: #111418;
--gray-50: #f7f8fa;
--blue-500: #2563eb;
--blue-300: #93c5fd;
/* Couche 2 : sémantique (l'API des composants) */
--color-bg: var(--gray-50);
--color-text: var(--gray-900);
--color-accent: var(--blue-500);
}Le bénéfice est immédiat en maintenance : un changement de teinte de marque se traite dans les primitives, un problème de contraste détecté en audit d’accessibilité se traite dans la couche sémantique. Les composants, eux, ne bougent pas — ils consomment var(--color-text).
Un composant qui référence une couleur brute est une dette technique qui s’ignore. Un composant qui référence un token sémantique est un contrat.
Le thème sombre comme redéfinition
Une fois cette séparation en place, le mode sombre devient une opération sans relief — ce qui est précisément l’objectif. Rien n’est dupliqué : seule la couche sémantique est redéfinie. Les primitives restent, les composants restent, seule la table de correspondance change.
1. Respecter la préférence système
Le point de départ est prefers-color-scheme, qui traduit la préférence exprimée par l’utilisateur au niveau de son système d’exploitation. L’ignorer revient à passer outre un réglage explicite.
@media (prefers-color-scheme: dark) {
:root {
--color-bg: var(--gray-900);
--color-text: var(--gray-50);
--color-accent: var(--blue-300);
}
}2. Autoriser un choix explicite
Un attribut posé sur l’élément racine permet de surcharger la préférence système lorsque l’utilisateur bascule manuellement. L’ordre des règles détermine la priorité : le choix explicite doit l’emporter dans les deux sens.
:root[data-theme='dark'] { /* mêmes redéfinitions */ }
@media (prefers-color-scheme: dark) {
:root:not([data-theme='light']) { /* mêmes redéfinitions */ }
}3. Éviter le flash au chargement
La seule ligne de JavaScript réellement nécessaire est un script bloquant placé dans le <head>, qui applique le thème mémorisé avant le premier rendu. Toute autre approche produit un flash de thème clair sur les pages chargées en mode sombre.
<script>
try {
var t = localStorage.getItem('theme');
if (t) document.documentElement.setAttribute('data-theme', t);
} catch (e) {}
</script>color-scheme: light ou dark sur :root ne relève pas du détail. C’est ce qui indique au navigateur d’adapter les contrôles de formulaire, les barres de défilement et les champs natifs. Sans cette déclaration, un thème sombre reste trahi par ses <select>.Ce que la méthode évite
| Symptôme fréquent | Cause | Réponse par les tokens |
|---|---|---|
Fichier dark.css qui diverge | Duplication de règles | Une seule feuille, redéfinition sémantique |
Accumulation de !important | Conflits de spécificité | Les composants ne lisent qu’une variable |
| Contraste insuffisant en sombre | Couleurs choisies au cas par cas | Correction centralisée sur un token |
| Flash clair au chargement | Thème appliqué après le rendu | Script bloquant dans le <head> |
Ce qu’il faut retenir
Un thème clair/sombre maintenable ne demande ni framework ni couche JavaScript : deux couches de tokens, une redéfinition sémantique, et un script de trois lignes contre le flash initial. L’essentiel du travail est une décision d’architecture prise avant d’écrire la première règle de style.
C’est la méthode que j’applique sur mes projets clients depuis plusieurs années, et le thème de ce site en est une application directe. La règle que je ne transige plus : aucun composant ne référence une primitive. Le jour où cette règle saute, la dette revient en six mois. — Simon Janvier
