Un écran d'ordinateur affichant une interface en thème sombre

Créer un thème sombre avec CSS variables et JavaScript

Publié le

Le thème sombre n'est plus une option : la plupart des systèmes d'exploitation et des navigateurs proposent désormais une préférence utilisateur, et de nombreux visiteurs l'activent. Proposer un thème sombre sur son site améliore le confort de lecture, réduit la fatigue oculaire en environnement peu éclairé et économise la batterie sur les écrans OLED.

Bonne nouvelle : pas besoin de dupliquer toute votre feuille de style. Avec les variables CSS (custom properties), un peu de JavaScript et deux ou trois bonnes pratiques d'accessibilité, vous pouvez mettre en place un thème sombre propre, persistant et respectueux des préférences système.

🎨 Pourquoi utiliser des variables CSS pour un thème sombre ?

Les variables CSS permettent de définir une valeur à un endroit et de la réutiliser partout ailleurs. Pour un thème, c'est idéal : au lieu d'écrire color: #1a1a1a; à cinquante endroits, vous écrivez color: var(--color-text); une seule fois. Changer de thème revient alors à redéfinir les valeurs des variables, sans toucher au reste du CSS.

  • Centralisation : toutes les couleurs sont regroupées au même endroit.
  • Maintenance : une seule modification se propage partout.
  • Flexibilité : on peut changer de thème à chaud, sans recharger la page.
  • Performance : aucune requête supplémentaire, aucun CSS dupliqué.

🧱 Étape 1 : Définir les variables du thème clair

On commence par déclarer les variables sur le sélecteur :root, qui correspond à l'élément <html>. Elles seront donc disponibles partout dans le document.

:root {
    /* Thème clair (par défaut) */
    --color-bg: #ffffff;
    --color-surface: #f4f5f7;
    --color-text: #1a1a1a;
    --color-muted: #5a5a5a;
    --color-accent: #d35400;
    --color-border: #e2e4e8;

    /* Indique au navigateur les thèmes supportés */
    color-scheme: light;
}

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

La propriété color-scheme est souvent oubliée : elle indique au navigateur quel schéma de couleurs appliquer aux éléments natifs (barres de défilement, champs de formulaire, <select>…). Sans elle, votre thème sombre aura toujours des scrollbars blanches, ce qui casse complètement l'effet.

🌙 Étape 2 : Redéfinir les variables pour le thème sombre

On crée ensuite une règle qui ne s'applique que lorsque l'attribut data-theme="dark" est présent sur <html>. On y redéfinit simplement les mêmes variables avec des valeurs sombres.

[data-theme="dark"] {
    --color-bg: #121212;
    --color-surface: #1e1e1e;
    --color-text: #f0f0f0;
    --color-muted: #a8a8a8;
    --color-accent: #ff8a3d;
    --color-border: #2e2e2e;

    color-scheme: dark;
}

Comme le reste du CSS utilise uniquement var(--color-...), tout le site bascule instantanément. Aucun autre sélecteur n'a besoin d'être modifié.

🖥️ Étape 3 : Respecter les préférences système

Un bon thème sombre ne s'impose pas : il s'adapte. La requête média prefers-color-scheme permet de détecter ce que l'utilisateur a choisi au niveau de son système d'exploitation.

@media (prefers-color-scheme: dark) {
    /* Appliqué seulement si l'utilisateur n'a pas choisi manuellement le thème clair */
    :root:not([data-theme="light"]) {
        --color-bg: #121212;
        --color-surface: #1e1e1e;
        --color-text: #f0f0f0;
        --color-muted: #a8a8a8;
        --color-accent: #ff8a3d;
        --color-border: #2e2e2e;

        color-scheme: dark;
    }
}

Le sélecteur :root:not([data-theme="light"]) est la pièce maîtresse. Il signifie : « applique le thème sombre système, sauf si l'utilisateur a explicitement choisi le thème clair ». Le choix manuel gagne toujours sur la préférence système, ce qui est exactement le comportement attendu.

🔘 Étape 4 : Créer un bouton de bascule accessible

Le bouton doit être compréhensible par tout le monde, y compris par les lecteurs d'écran. On utilise donc un vrai <button> avec un libellé explicite, et on fera évoluer son aria-label et son état via JavaScript.

<button
    type="button"
    id="theme-toggle"
    class="theme-toggle"
    aria-pressed="false"
    aria-label="Activer le thème sombre"
>
    <span aria-hidden="true">🌙</span>
    <span>Thème sombre</span>
</button>

Pourquoi aria-pressed ? Il transforme le bouton en bouton à bascule au sens ARIA et indique s'il est actif ou non. Un lecteur d'écran annoncera par exemple « Thème sombre, bouton bascule, enfoncé ». C'est plus fiable que de se contenter de changer le texte visible.

⚙️ Étape 5 : Écrire le JavaScript

Le script doit répondre à trois besoins : appliquer le bon thème au chargement, basculer entre les deux thèmes au clic, et mémoriser le choix de l'utilisateur avec localStorage.

const root = document.documentElement;
const toggle = document.querySelector('#theme-toggle');
const STORAGE_KEY = 'theme';

/**
 * Détermine le thème à appliquer :
 * 1. Le choix mémorisé (localStorage)
 * 2. Sinon, la préférence système
 * 3. Sinon, le thème clair par défaut
 */
function getPreferredTheme() {
    const stored = localStorage.getItem(STORAGE_KEY);
    if (stored === 'light' || stored === 'dark') {
        return stored;
    }
    return window.matchMedia('(prefers-color-scheme: dark)').matches
        ? 'dark'
        : 'light';
}

/**
 * Applique le thème à la page et met à jour le bouton.
 */
function applyTheme(theme) {
    const isDark = theme === 'dark';

    root.setAttribute('data-theme', theme);
    toggle.setAttribute('aria-pressed', String(isDark));
    toggle.setAttribute(
        'aria-label',
        isDark ? 'Activer le thème clair' : 'Activer le thème sombre'
    );
}

// Application au chargement
applyTheme(getPreferredTheme());

// Bascule au clic
toggle.addEventListener('click', () => {
    const current = root.getAttribute('data-theme');
    const next = current === 'dark' ? 'light' : 'dark';

    applyTheme(next);
    localStorage.setItem(STORAGE_KEY, next);
});

// Suivre les changements de préférence système
window
    .matchMedia('(prefers-color-scheme: dark)')
    .addEventListener('change', (event) => {
        // On ne suit le système que si l'utilisateur n'a rien choisi manuellement
        if (!localStorage.getItem(STORAGE_KEY)) {
            applyTheme(event.matches ? 'dark' : 'light');
        }
    });

Explication du code JavaScript

  • Récupération de l'élément racine
    const root = document.documentElement;
    On cible directement <html>, l'élément sur lequel on pose l'attribut data-theme.
  • Lecture de la préférence
    localStorage.getItem(STORAGE_KEY)
    Si l'utilisateur a déjà cliqué sur le bouton, on retrouve son choix. Sinon, on interroge le système via matchMedia.
  • Application du thème
    root.setAttribute('data-theme', theme);
    C'est cet attribut qui déclenche la règle CSS [data-theme="dark"] écrite à l'étape 2.
  • Mise à jour de l'état du bouton
    toggle.setAttribute('aria-pressed', String(isDark));
    On synchronise l'état visuel et l'état ARIA du bouton.
  • Persistance du choix
    localStorage.setItem(STORAGE_KEY, next);
    Le choix est conservé d'une visite à l'autre. Seules les valeurs "light" et "dark" sont stockées.
  • Écoute du changement système
    matchMedia(...).addEventListener('change', ...)
    Si l'utilisateur change de thème au niveau du système (par exemple avec la bascule automatique au coucher du soleil), la page suit — mais uniquement s'il n'a pas fixé son choix manuellement.

⚡ Étape 6 : Éviter le flash de thème clair (FOUC)

Il reste un défaut invisible dans la démo mais très visible sur un vrai site : au chargement, le HTML s'affiche en thème clair pendant quelques millisecondes avant que le JavaScript ne s'exécute et n'applique le thème sombre. C'est le fameux flash of unstyled content.

La solution consiste à placer un petit script inline, dans le <head>, avant le rendu du <body>.

<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>

Ce script s'exécute immédiatement, avant que le navigateur ne peigne la page. L'attribut data-theme est donc déjà en place au premier rendu : aucun flash. La fonction englobante (function () { ... })(); évite de polluer la portée globale.

⚠️ À noter : en plaçant ce script dans le <head>, on introduit un léger coût de parsing bloquant. Ici, c'est négligeable (quelques lignes), et le gain en confort visuel est bien supérieur au coût.

🧪 Exemple interactif

Cliquez sur le bouton ci-dessous pour basculer le thème de cette petite carte. Le comportement reproduit exactement la logique décrite dans l'article.

Ma carte de démonstration

Ce contenu, sa couleur de fond et ses bordures reposent entièrement sur des variables CSS redéfinies par l'attribut data-theme.

Un lien de démonstration

✅ Bonnes pratiques à retenir

  • Ne jamais se reposer uniquement sur la couleur. Un lien ne doit pas être identifié seulement par sa teinte : ajoutez un soulignement ou une graisse différente.
  • Vérifier les contrastes. Le ratio minimum recommandé par les WCAG est de 4,5:1 pour le texte courant et 3:1 pour le texte large. Un thème sombre mal calibré peut être moins lisible qu'un thème clair.
  • Utiliser color-scheme. Sans elle, les éléments natifs du navigateur resteront en clair et casseront l'illusion.
  • Respecter prefers-reduced-motion. Désactivez les transitions de couleur si l'utilisateur a demandé à limiter les animations.
  • Stocker uniquement "light" ou "dark". Évitez de stocker "auto" : il est plus simple de supprimer la clé du localStorage pour revenir au comportement système.
  • Prévoir un troisième état (optionnel). Sur un vrai site, vous pouvez proposer trois choix : clair, sombre, automatique. Cela demande un cycle à trois états dans le bouton.

🧭 En résumé

Un thème sombre réussi repose sur trois piliers : des variables CSS pour centraliser les couleurs, une préférence système respectée via prefers-color-scheme, et un choix utilisateur persistant stocké dans localStorage. Le tout tient en une cinquantaine de lignes, sans dépendance externe.

Une fois cette base en place, vous pouvez l'enrichir : thème par section, thème par composant, voire plusieurs palettes (sépia, contraste élevé…). La logique reste exactement la même.

Sources

Mots clés

CSS JavaScript Accessibilité

Vous travaillez sur un thème sombre ou un design system ?

Calibrer des contrastes, organiser des variables CSS, ou simplement décider jusqu'où pousser la personnalisation… parfois un regard extérieur aide à voir ce qu'on ne voit plus.

Prenons contact
Développeur avec une tasse de café
Une femme utilise son ordinateur portable, les doigts sur le clavier Changer la couleur d'une icône au clic avec HTML et CSS

Apprenez à changer la couleur d'une icône SVG au clic uniquement avec HTML et CSS, sans utiliser JavaScript.

Une personne en train de taper sur le clavier d'un ordinateur portable Créer une barre de recherche interactive avec JavaScript

Découvrez comment créer une barre de recherche interactive avec HTML et JavaScript. Tutoriel étape par étape.

Homme assis derrière son écran. Comprendre la console JavaScript : Au-delà de console.log

Allez au-delà de console.log() ! Découvrez comment utiliser les autres méthodes de la console JavaScript (warn, error, table, group, time) pour un débogage plus efficace.