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

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
-
Mai 2025 HTML CSS JS 🌙 Mode Sombre 🧩 UX ♿ Accessibilité
-
Mai 2025 🌙 Mode Sombre 🧩 UX ♿ Accessibilité
1. Gérer les thèmes clair/sombre : bases, accessibilité et bonnes pratiques
-
Mai 2025 HTML CSS 🌙 Mode Sombre 🧩 UX ♿ Accessibilité
2. Gérer les thèmes clair/sombre en HTML et CSS : solution native
-
Mai 2025 HTML CSS JS 🌙 Mode Sombre 🧩 UX ♿ Accessibilité
3. Gérer les thèmes clair/sombre avec JavaScript : offrir un vrai contrôle utilisateur
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 :
-
Mai 2025 CSS ♿ Accessibilité 🧩 UX
CSS : Améliorez l'accessibilité en respectant les préférences de contraste avec
prefers-contrast
-
Mai 2025 CSS ♿ Accessibilité 🧩 UX
CSS : Adapter les animations aux préférences des utilisateurs avec
prefers-reduced-motion
À 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.