BrowserUX Blog

Développement Front-End

HTML CSS JS 🌙 Mode Sombre 🧩 UX ♿ Accessibilité

Gérer les thèmes clair/sombre avec JavaScript : offrir un vrai contrôle utilisateur

Theme Light/Dark

I. HTML : Structurer et optimiser pour les thèmes Light/Dark

1. Ajout du bouton de changement de thème manuel et accessiblité

Lorsqu’on propose un bouton pour changer de thème, l’accessibilité doit être une priorité. Il est essentiel d’utiliser une balise sémantique comme <button> et de fournir un libellé explicite via l’attribut aria-label.


<button id="theme-toggle" aria-label="Activer le mode sombre">🌓</button>

L’attribut aria-label est ici fondamental : il permet aux technologies d’assistance (lecteurs d’écran, etc.) d’annoncer clairement l’action attendue. Sans cet attribut, un utilisateur malvoyant n'aurait aucune indication sur la fonction du bouton – surtout si celui-ci se limite à une icône.

En décrivant l’action ("Changer de thème", "Activer le mode sombre", etc.), on garantit une expérience inclusive à tous les utilisateurs.

2. Utilisation de l'attribut data-*

Une approche propre et évolutive consiste à utiliser un attribut personnalisé comme data-theme sur la balise html (ou body) pour refléter le thème actif :


<html data-theme="light">

Cet attribut permet à la fois :

  • de cibler des styles CSS conditionnels ([data-theme="dark"] { ... }),
  • et de piloter la logique JavaScript (lecture, bascule, etc.).

Ce point sert avant tout à poser la logique qui sera utilisée en JavaScript : il n’est donc pas nécessaire d’ajouter l’attribut data-theme manuellement dans le HTML, il sera appliqué dynamiquement par le script.

3. Gestion des images en fonction du thème

Dans le cas d’éléments visuels comme les logos, il est fréquent de vouloir afficher une version claire ou sombre selon le thème actif. Cependant, contrairement à l’approche décrite dans l’article précédent qui repose uniquement sur les préférences système (prefers-color-scheme), ici nous gérons les thèmes de manière dynamique via JavaScript.

Cela change la façon dont on gère les images.

Pourquoi ne pas utiliser <picture> avec media="(prefers-color-scheme: dark)"

L’approche suivante semble naturelle, mais elle est inadaptée dans un système de thème contrôlé manuellement :


<picture>
    <source srcset="/images/logo-dark.png" media="(prefers-color-scheme: dark)">
    <img src="/images/logo.png" alt="Logo du site">
</picture>

Le problème ici est que le navigateur choisit l’image au moment du chargement de la page, en fonction de la préférence système. Il ne prend pas en compte les changements dynamiques effectués plus tard via JavaScript.

Résultat : si l'utilisateur change de thème manuellement à l’aide d’un bouton, l’image ne se mettra pas à jour, car la source a déjà été fixée par le navigateur.

Solution : une seule balise <img> avec une classe dédiée

Pour gérer dynamiquement l’image, on utilise plutôt une simple balise <img> avec une classe spécifique, comme ceci :


<img class="has-dark" src="/images/logo.png" alt="Logo du site">

On utilisera alors JavaScript pour remplacer dynamiquement le src lorsque le thème passe en sombre, en ajoutant -dark à logo.png pour que soit affiché le fichier alternatif logo-dark.png.

II. CSS : Utilisation de data-theme pour le changement de thème

Pour que le changement de thème soit fluide, maintenable et contrôlable manuellement, il est préférable de structurer les styles autour d’un attribut HTML central : data-theme. Cet attribut, appliqué sur la balise <html>, sert de point d’ancrage pour toute la logique CSS du thème clair/sombre. Il permet d’éviter de dupliquer les styles et de centraliser les règles de manière propre.

1. Thème manuel via data-theme

Comme vu dans la partie HTML, si l’on souhaite permettre à l’utilisateur de choisir manuellement son thème via un bouton, il suffit d’appliquer dynamiquement un attribut data-theme="light" ou data-theme="dark" sur la balise <html>.

Cela permet ensuite de piloter les couleurs globales de l’interface via des variables CSS, sans avoir à réécrire tous les styles à chaque fois :


:root[data-theme="dark"] {
	--bg-color: #121212;
	--text-color: #f0f0f0;
}

Ce modèle rend le changement de thème très simple : JavaScript n’a qu’à modifier la valeur de l’attribut data-theme, et les styles s’adaptent automatiquement grâce aux variables CSS.

2. Éviter les conflits : priorité au thème manuel

Dans un projet qui combine à la fois détection système (prefers-color-scheme) et contrôle manuel (via data-theme), il est important que le choix de l’utilisateur prime sur les réglages automatiques.

Par défaut, si on utilise une media query comme :


@media (prefers-color-scheme: dark) {
    :root {
        --bg-color: #121212;
        --text-color: #f0f0f0;
    }
}

Celle-ci sera évaluée par le navigateur dès le chargement, et s’appliquera même si l’utilisateur choisit un thème ensuite avec le bouton JS.

Pour éviter ce conflit, tu peux limiter l’effet de la media query uniquement si aucun thème n’a été explicitement défini, grâce au sélecteur :not([data-theme]) :


@media (prefers-color-scheme: dark) {
    :root:not([data-theme]) {
        --bg-color: #121212;
        --text-color: #f0f0f0;
    }
}

Dans le cas de cet article, ce fallback n’est pas strictement nécessaire, car data-theme est ajouté dès la première visite grâce au JavaScript, que ce soit via un thème enregistré ou une détection système.

Cependant, ajouter cette condition est une bonne pratique, car elle permet de gérer correctement les cas où :

  • JavaScript ne fonctionne pas ou est désactivé,
  • le chargement JS est retardé,
  • ou une erreur empêche data-theme d’être appliqué à temps.

Cela renforce la robustesse du système et améliore l’accessibilité dès les premières millisecondes du rendu.

III. JavaScript : Gestion des thèmes en donnant le choix

Dans cette partie, nous allons mettre en place toute la logique JavaScript permettant à un site :

  • d’appliquer un thème clair ou sombre selon la préférence utilisateur ou système,
  • de mémoriser le choix de l’utilisateur pour les visites suivantes,
  • de mettre à jour dynamiquement les images du site selon le thème actif.

L’ensemble repose sur un principe simple : contrôler dynamiquement l’attribut data-theme appliqué à la balise <html>, puis ajuster le rendu en fonction.

1. updateImagesByTheme() : synchroniser les images avec le thème

Cette fonction parcourt toutes les images marquées avec la classe has-dark, et met à jour leur src en fonction du thème actuel :


function updateImagesByTheme() {

On vérifie si le thème actuel est en mode sombre :


const isDark = document.documentElement.getAttribute("data-theme") === "dark";

On sélectionne uniquement les images qui ont une version alternative sombre (classe has-dark) et parcourt chaque image concernée :


document.querySelectorAll('img.has-dark').forEach((img) => {

On récupère la source d’origine de l’image, si elle est déjà stockée dans data-src, on la réutilise, sinon, on lit l’attribut src actuel :


const originalSrc = img.dataset.src || img.getAttribute("src");

Si ce n’est pas encore fait, on enregistre le src d’origine dans data-src :


if (!img.dataset.src) img.dataset.src = originalSrc;

On détermine dynamiquement le src à utiliser selon le thème :

  • En mode dark : remplace .ext par -dark.ext (ex : image.jpg → image-dark.jpg)
  • En mode light : revient à l’image d’origine

img.src = isDark
    ? originalSrc.replace(/(\.\w+)$/, "-dark$1")
    : img.dataset.src;

Fonction finale :


function updateImagesByTheme() {
    const isDark = document.documentElement.getAttribute("data-theme") === "dark";
    document.querySelectorAll('img.has-dark').forEach((img) => {
    const originalSrc = img.dataset.src || img.getAttribute("src");
    if (!img.dataset.src) img.dataset.src = originalSrc;
    img.src = isDark
        ? originalSrc.replace(/(\.\w+)$/, "-dark$1")
        : img.dataset.src;
    });
}

Ce système permet de charger dynamiquement une version alternative de chaque image sans avoir recours à <picture> ni à des media queries CSS.

2. applyTheme(theme) : changer de thème et mettre à jour l’interface

Cette fonction applique un thème donné (light ou dark) en modifiant l’attribut data-src sur <html>, puis appelle updateImagesByTheme() pour que l’affichage des images suive :


function applyTheme(theme) {

On définit l’attribut data-theme sur <html> (impacte le CSS global) :


document.documentElement.setAttribute('data-theme', theme);

On met à jour les images affichées selon le theme sauvegardé :


updateImagesByTheme();

On met à jour dynamiquement le contenu du bouton (icône 🌞 ou 🌙) :


const toggleThemeButton = document.getElementById('theme-toggle');
toggleThemeButton.textContent = theme === 'dark' ? '🌞' : '🌙';

On met à jour le aria-label du button pour les lecteurs d'écran :


const label = theme === 'dark' ? 'Activer le mode clair' : 'Activer le mode sombre';
toggleThemeButton.setAttribute('aria-label', label);

Fonction finale :


function applyTheme(theme) {
    document.documentElement.setAttribute('data-theme', theme);
    updateImagesByTheme();
    const toggleThemeButton = document.getElementById('theme-toggle');
    if (toggleThemeButton) {
        toggleThemeButton.textContent = theme === 'dark' ? '🌞' : '🌙';
        const label = theme === 'dark' ? 'Activer le mode clair' : 'Activer le mode sombre';
        toggleThemeButton.setAttribute('aria-label', label);
    }
}

Cette fonction est le point central de la logique de thème : tous les changements de thème passent par elle.

3. initializeTheme() : déterminer le thème au chargement de la page

Cette fonction est appelée au chargement. Elle choisit quel thème appliquer en suivant une logique simple :

  • Si un thème est enregistré dans le localStorage, on l’utilise.
  • Sinon, on regarde si le système de l’utilisateur est configuré en sombre (prefers-color-scheme).
  • Par défaut, on applique le thème clair.

function initializeTheme() {

Le localStorage est un mécanisme de stockage local permanent fourni par le navigateur. Contrairement aux cookies, il n’expire pas automatiquement et reste disponible même après la fermeture du navigateur. On l’utilise ici pour mémoriser la préférence de l’utilisateur et la restaurer automatiquement lors de ses futures visites.

On récupère le thème précédemment enregistré dans le localStorage (si l'utilisateur a déjà fait un choix) :


const saved = localStorage.getItem('theme');

On utilise l'API matchMedia pour savoir si le système de l'utilisateur est en mode sombre :


const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;

Détermine le thème à appliquer :

  • Si un thème est enregistré, on l'utilise,
  • Sinon, on applique 'dark' si l'utilisateur préfère un thème sombre, ou 'light' par défaut.

const theme = saved || (prefersDark ? 'dark' : 'light');

On applique le thème sélectionné et met à jour les images associées :


applyTheme(theme);

Fonction finale :


function initializeTheme() {
    const saved = localStorage.getItem('theme');
    const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
    const theme = saved || (prefersDark ? 'dark' : 'light');
    applyTheme(theme);
}

Cette fonction garantit une expérience cohérente dès le chargement, tout en respectant la préférence utilisateur.

4. Gestion du bouton #theme-toggle

Enfin, on ajoute un écouteur de clic sur le bouton de changement de thème :


document.getElementById('theme-toggle')?.addEventListener('click', () => {

On récupère le thème actuellement appliqué via l'attribut data-theme sur la balise <html> :


const current = document.documentElement.getAttribute('data-theme');

On détermine le thème opposé, si le thème est "dark", on passe à "light", et inversement :


const newTheme = current === 'dark' ? 'light' : 'dark';

On enregistre le nouveau thème dans le localStorage pour qu'il soit conservé lors des prochaines visites :


localStorage.setItem('theme', newTheme);

On applique le thème sélectionné et met à jour les images associées :


applyTheme(newTheme);

addEventListener final


document.getElementById('theme-toggle')?.addEventListener('click', () => {
    const current = document.documentElement.getAttribute('data-theme');
    const newTheme = current === 'dark' ? 'light' : 'dark';
    localStorage.setItem('theme', newTheme);
    applyTheme(newTheme);
});

5. Initialisation au chargement

On termine en appelant initializeTheme() dès que le script est chargé, pour appliquer immédiatement le thème adéquat.


initializeTheme();

6. Code final

Avec ces quelques fonctions JavaScript :

  • le site s’adapte automatiquement au thème système ou à la préférence enregistrée,
  • les utilisateurs peuvent changer de thème manuellement à tout moment,
  • les images changent dynamiquement en cohérence avec l'interface.

Ce système est simple, robuste, et facilement extensible pour d'autres éléments (icônes, graphiques, arrière-plans, etc.).


function updateImagesByTheme() {
    const isDark = document.documentElement.getAttribute("data-theme") === "dark";
    document.querySelectorAll('img.has-dark').forEach((img) => {
    const originalSrc = img.dataset.src || img.getAttribute("src");
    if (!img.dataset.src) img.dataset.src = originalSrc;
    img.src = isDark
        ? originalSrc.replace(/(\.\w+)$/, "-dark$1")
        : img.dataset.src;
    });
}

function applyTheme(theme) {
    document.documentElement.setAttribute('data-theme', theme);
    updateImagesByTheme();
    const toggleThemeButton = document.getElementById('theme-toggle');
    if (toggleThemeButton) {
        toggleThemeButton.textContent = theme === 'dark' ? '🌞' : '🌙';
        const label = theme === 'dark' ? 'Activer le mode clair' : 'Activer le mode sombre';
        toggleThemeButton.setAttribute('aria-label', label);
    }
}

function initializeTheme() {
    const saved = localStorage.getItem('theme');
    const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
    const theme = saved || (prefersDark ? 'dark' : 'light');
    applyTheme(theme);
}

document.getElementById('theme-toggle')?.addEventListener('click', () => {
    const current = document.documentElement.getAttribute('data-theme');
    const newTheme = current === 'dark' ? 'light' : 'dark';
    localStorage.setItem('theme', newTheme);
    applyTheme(newTheme);
});

initializeTheme();

IV. Conclusion

Dans cette troisième partie, nous avons vu comment JavaScript permet de rendre le système de thème clair/sombre interactif, dynamique et personnalisable.

Grâce à quelques fonctions bien structurées :

  • le thème peut être appliqué automatiquement selon les préférences utilisateur ou système,
  • un bouton accessible permet de basculer facilement entre les modes,
  • les images elles-mêmes s’adaptent dynamiquement pour rester cohérentes avec l’environnement visuel.

Ce système repose sur une logique simple : contrôler l’attribut data-theme et centraliser l’état du thème dans le DOM et dans le localStorage.

Ce modèle offre plusieurs bénéfices :

  • Persistant : le thème reste appliqué entre les visites
  • Interactivement contrôlable : via une interface utilisateur
  • Visuellement cohérent : les images s’alignent avec le thème

Cette fondation solide peut ensuite être enrichie par des approches plus avancées, comme un thème adaptatif basé sur l’heure ou des préférences utilisateur stockées côté serveur.

Le guide complet

Logo browserux.css browserux.css est un fichier de base CSS pensé comme une alternative moderne aux resets classiques et à Normalize.css, axé sur l'expérience utilisateur et l'accessibilité. Il pose des fondations accessibles, cohérentes et adaptées aux usages actuels du web : browserux.css

Aller plus loin

Si vous souhaitez approfondir la prise en compte des préférences utilisateurs dans vos interfaces, voici quelques ressources qui pourraient vous intéresser :

À propos

Ce blog a été conçu comme une extension naturelle des projets BrowserUX Starter et browserux.css.

Il a pour objectif de fournir des ressources complémentaires, des astuces ciblées et des explications détaillées autour des choix techniques, des bonnes pratiques et des principes d’accessibilité qui structurent ces outils.

Chaque article ou astuce vient éclairer un aspect précis du front-end moderne (CSS, accessibilité, UX, performance…), avec une volonté claire : expliquer le pourquoi derrière chaque règle pour encourager une intégration plus réfléchie et durable dans vos projets.