Skip to main content

Chapitre 66 : Arbitrage : Architecture de thèmes (Data-attributes vs Classes)

Comparaison des stratégies de basculement de thèmes au niveau du DOM pour des architectures CSS scalables.

En tant qu'architecte front-end, le choix de la méthode de déclenchement d'un thème ne doit pas être laissé au hasard ou à la préférence esthétique. Bien que le résultat visuel soit identique pour l'utilisateur, l'impact sur la maintenance, la spécificité CSS, l'accessibilité et l'intégration avec JavaScript varie considérablement entre l'utilisation de classes CSS et l'utilisation d'attributs de données (data-attributes).

L'approche par Classes CSS

1. Quoi

L'approche par classes consiste à injecter une ou plusieurs classes (ex: .theme-dark, .theme-high-contrast) sur un élément racine, généralement <html> ou <body>. Le CSS utilise ensuite des descendants combinators pour redéfinir les variables CSS (Custom Properties).

2. Pourquoi

C'est la méthode historique et la plus simple. Elle est idéale pour des projets de petite taille ou des systèmes de thèmes binaires (Light/Dark) où la complexité est minimale. Elle bénéficie d'une compatibilité totale avec tous les navigateurs, même les plus anciens.

3. Comment

A. Syntaxe de base

:root {
--bg-color: #ffffff;
--text-color: #000000;
}

/* Basculement via classe */
.theme-dark {
--bg-color: #1a1a1a;
--text-color: #f0f0f0;
}

body {
background-color: var(--bg-color);
color: var(--text-color);
}

B. Cas concret (Architecture Senior)

Dans un projet professionnel, on évite de redéfinir chaque propriété. On utilise un système de Design Tokens où la classe ne fait que modifier la valeur des tokens de base.

/* Tokens de base (Primitives) */
:root {
--color-gray-100: #f8f9fa;
--color-gray-900: #212529;
--color-blue-500: #007bff;
}

/* Tokens Sémantiques (Thème Light par défaut) */
:root {
--surface-primary: var(--color-gray-100);
--text-main: var(--color-gray-900);
--accent: var(--color-blue-500);
}

/* Overrides pour le thème Dark */
.theme-dark {
--surface-primary: var(--color-gray-900);
--text-main: var(--color-gray-100);
/* L'accent reste identique, assurant la cohérence de marque */
}

.card {
background-color: var(--surface-primary);
color: var(--text-main);
border: 1px solid var(--accent);
}

C. Limitations

  • Pollution de la liste de classes : Si vous avez plusieurs thèmes (Dark, Solarized, High Contrast, Brand-A, Brand-B), la gestion des classes peut devenir confuse.
  • Conflits de spécificité : Les classes augmentent la spécificité. Si vous mélangez des classes de thèmes avec des frameworks CSS (comme Tailwind), vous pouvez vous retrouver dans des guerres de spécificité complexes.
  • Sémantique faible : Une classe est destinée au style. Utiliser une classe pour porter un état global de l'application est un détournement sémantique.

4. Zone de Danger

Erreur commune : Appliquer la classe de thème sur chaque composant individuellement. <div class="card theme-dark">...</div> Cela rend le changement de thème global impossible sans parcourir tout le DOM.

Bonne pratique : Appliquer la classe uniquement sur l'élément racine (<html>). Le CSS se charge de la propagation via les variables.


L'approche par Data-Attributes

1. Quoi

L'utilisation d'attributs de données personnalisés (ex: data-theme="dark") permet de stocker l'état du thème dans un attribut HTML5 dédié. Le CSS cible cet attribut via un sélecteur d'attribut.

2. Pourquoi

Cette approche sépare clairement la présentation (classes) de l'état/configuration (data-attributes). Elle est extrêmement puissante pour les systèmes multi-thèmes (plus de deux options) et facilite grandement la manipulation via JavaScript.

3. Comment

A. Syntaxe de base

[data-theme="light"] {
--bg: white;
}

[data-theme="dark"] {
--bg: black;
}

B. Cas concret (Architecture Senior)

Voici une implémentation robuste gérant plusieurs thèmes et l'intégration avec le système d'exploitation.

/* 1. Définition des palettes */
:root {
--palette-light-bg: #ffffff;
--palette-dark-bg: #121212;
--palette-ocean-bg: #001f3f;
}

/* 2. Mapping sémantique basé sur l'attribut */
[data-theme="light"] {
--bg-app: var(--palette-light-bg);
}

[data-theme="dark"] {
--bg-app: var(--palette-dark-bg);
}

[data-theme="ocean"] {
--bg-app: var(--palette-ocean-bg);
}

/* 3. Fallback pour les utilisateurs n'ayant pas encore choisi */
:root:not([data-theme]) {
--bg-app: var(--palette-light-bg);
}

/* 4. Gestion intelligente du mode système via Media Query */
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) {
--bg-app: var(--palette-dark-bg);
}
}

body {
background-color: var(--bg-app);
transition: background-color 0.3s ease;
}

C. Limitations

  • Performance : Les sélecteurs d'attributs sont légèrement plus lents que les sélecteurs de classes, mais cette différence est négligeable sur les navigateurs modernes, surtout lorsqu'ils sont appliqués à la racine.
  • Verbosité : L'écriture [data-theme="..."] est plus longue que .theme-....

4. Zone de Danger

Erreur commune : Utiliser des attributs non standards ou trop génériques (ex: theme="dark" sans le préfixe data-). Cela peut invalider le HTML ou entrer en conflit avec des attributs futurs du standard HTML.

Bonne pratique : Toujours utiliser le préfixe data-. Pour un projet d'entreprise, préférez un nom spécifique comme data-app-theme pour éviter les collisions avec des bibliothèques tierces.

Arbitrage Architecture de Thèmes


Comparaison Technique et Arbitrage

Pour un profil Senior, le choix se base sur des critères de scalabilité et de maintenance.

CritèreClasses CSSData-AttributesGagnant
SémantiqueFaible (Style → Style)Forte (État → Style)Data-Attributes
ExtensibilitéLinéaire (Ajout de classes)Modulaire (Valeurs d'attributs)Data-Attributes
JS APIclassList.add/removesetAttribute/getAttributeÉgalité
SpécificitéMoyenne (0,1,0)Moyenne (0,1,0)Égalité
PerformanceOptimaleQuasi-optimaleClasses
Lisibilité DOMPolluée si multi-thèmesPropre et expliciteData-Attributes

Quand choisir quoi ?

  1. Choisissez les Classes si :

    • Vous avez un besoin strictement binaire (Light/Dark).
    • Vous travaillez sur un projet legacy avec un support navigateur très ancien.
    • Le thème est géré par un système de classes utilitaires (type Tailwind) où le thème n'est qu'une variante de composant.
  2. Choisissez les Data-Attributes si :

    • Vous prévoyez plus de 2 thèmes (ex: Dark, Light, High-Contrast, Midnight).
    • Vous construisez un Design System utilisé par plusieurs applications.
    • Vous voulez séparer l'état de configuration de l'application de ses styles de mise en page.
    • Vous souhaitez faciliter le debugging en voyant d'un coup d'œil l'état global dans l'inspecteur DOM : <html data-theme="ocean">.

Questions clés

1. Pourquoi les data-attributes sont-ils préférables pour un système multi-thèmes ?

Découvrir la réponse

Parce qu'ils permettent de définir une seule "clé" (data-theme) avec plusieurs "valeurs" (light, dark, blue, red). Avec les classes, il faudrait gérer l'ajout et la suppression mutuelle de plusieurs classes (ex: supprimer .theme-light pour ajouter .theme-dark), ce qui est plus sujet aux erreurs en JavaScript.

2. Quel est l'impact sur la spécificité CSS entre [data-theme="dark"] .card et .theme-dark .card ?

Découvrir la réponse

L'impact est identique. Les sélecteurs d'attributs et les sélecteurs de classe ont le même poids dans le calcul de la spécificité CSS (0,1,0). Le choix n'influence donc pas la priorité des styles.

3. Comment gérer le "Flash of Unstyled Content" (FOUC) lors du changement de thème au chargement de la page ?

Découvrir la réponse

Le script de détection du thème (localStorage ou préférence système) doit être placé dans le <head> du document, avant le rendu du <body>. En appliquant le data-theme ou la classe sur l'élément <html> immédiatement, le navigateur peut calculer les variables CSS avant le premier paint.

4. Peut-on combiner les deux approches ?

Découvrir la réponse

Oui, mais c'est déconseillé pour la cohérence. On pourrait utiliser un data-theme pour le thème global et des classes pour des thèmes locaux (ex: une section spécifique de la page qui doit être forcée en mode sombre). Cependant, il est préférable d'utiliser des attributs de données imbriqués pour rester cohérent.


Mise en pratique

Exercice 1 : Reproduction guidée (Niveau Junior/Intermédiaire)

Objectif : Créer un basculement simple entre deux thèmes en utilisant des data-attributes.

  1. Créez un fichier HTML avec un bouton "Toggle Theme".
  2. Définissez deux thèmes : light (fond blanc, texte noir) et dark (fond noir, texte blanc) via des variables CSS.
  3. Écrivez un script JS qui alterne la valeur de data-theme sur l'élément <html> entre "light" et "dark".
Découvrir la solution commentée
<!DOCTYPE html>
<html data-theme="light">
<head>
<style>
:root {
--bg: #ffffff;
--text: #000000;
}
[data-theme="dark"] {
--bg: #121212;
--text: #ffffff;
}
body {
background-color: var(--bg);
color: var(--text);
font-family: sans-serif;
transition: 0.3s;
}
</style>
</head>
<body>
<button id="toggle">Changer de thème</button>

<script>
const btn = document.getElementById('toggle');
const root = document.documentElement;

btn.addEventListener('click', () => {
// On récupère la valeur actuelle
const currentTheme = root.getAttribute('data-theme');
// On bascule vers l'autre
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
root.setAttribute('data-theme', newTheme);
});
</script>
</body>
</html>

Exercice 2 : Adaptation (Niveau Intermédiaire/Senior)

Objectif : Étendre le système précédent pour supporter trois thèmes : light, dark et forest (fond vert foncé, texte crème).

  1. Ajoutez le thème forest dans votre CSS.
  2. Modifiez le JavaScript pour que le bouton cycle à travers les trois thèmes dans l'ordre : Light → Dark → Forest → Light.
  3. Assurez-vous que le changement est fluide.
Découvrir la solution commentée
<style>
:root {
--bg: #ffffff;
--text: #000000;
}
[data-theme="dark"] {
--bg: #121212;
--text: #ffffff;
}
[data-theme="forest"] {
--bg: #1b3022;
--text: #f5f5dc;
}
body {
background-color: var(--bg);
color: var(--text);
transition: background-color 0.4s ease;
}
</style>

<script>
const btn = document.getElementById('toggle');
const root = document.documentElement;
const themes = ['light', 'dark', 'forest'];
let currentIndex = 0;

btn.addEventListener('click', () => {
// Incrémentation cyclique de l'index
currentIndex = (currentIndex + 1) % themes.length;
root.setAttribute('data-theme', themes[currentIndex]);
});
</script>

Exercice 3 : Conception (Niveau Senior)

Objectif : Implémenter un système de thèmes "Hybride" respectant les préférences système tout en permettant un override utilisateur persistant.

Contraintes :

  1. Par défaut, si l'utilisateur n'a rien choisi, le site doit suivre prefers-color-scheme.
  2. Si l'utilisateur clique sur "Light" ou "Dark", ce choix doit être enregistré dans le localStorage et primer sur le système.
  3. Ajoutez un bouton "Reset to System" pour supprimer le choix utilisateur et revenir au mode automatique.
  4. Utilisez des data-attributes pour l'implémentation.
Découvrir la solution commentée
<!DOCTYPE html>
<html data-theme="system">
<head>
<style>
/* Palette Primitives */
:root {
--white: #ffffff;
--black: #121212;
--text-light: #333;
--text-dark: #eee;
}

/* Mapping Sémantique */
[data-theme="light"] {
--bg: var(--white);
--text: var(--text-light);
}
[data-theme="dark"] {
--bg: var(--black);
--text: var(--text-dark);
}

/* Gestion du mode system via CSS */
[data-theme="system"] {
--bg: var(--white);
--text: var(--text-light);
}
@media (prefers-color-scheme: dark) {
[data-theme="system"] {
--bg: var(--black);
--text: var(--text-dark);
}
}

body {
background-color: var(--bg);
color: var(--text);
transition: 0.3s;
}
</style>
</head>
<body>
<button onclick="setTheme('light')">Light</button>
<button onclick="setTheme('dark')">Dark</button>
<button onclick="setTheme('system')">System</button>

<script>
const root = document.documentElement;

// 1. Initialisation immédiate pour éviter le flash (FOUC)
const savedTheme = localStorage.getItem('user-theme') || 'system';
root.setAttribute('data-theme', savedTheme);

function setTheme(theme) {
// Mise à jour du DOM
root.setAttribute('data-theme', theme);
// Persistance
localStorage.setItem('user-theme', theme);
}
</script>
</body>
</html>