Gérer des thèmes graphiques dans un site Web (v2)

TL;DR

Intégrer un button de choix du thème graphique, c’est bien ; proposer un menu de sélection du thème permettant de synchroniser à tout moment avec le thème système, c’est mieux !

J’aime beaucoup la façon dont le site MDN Web Docs de Mozilla a pensé et implémenté la fonctionnalité de sélection du thème graphique. Tant et si bien que j’ai décidé de le reproduire au sein de mon site.

Le code final est disponible en fin d’article.

Table des matières

Introduction

Cet article s’inscrit dans la suite de mon précédent billet “Supporter le “mode sombre” dans un site Hugo”.

Peu après l’avoir publié, je suis tombé sur des ressources en ligne et j’ai eu des retours de personnes soulevant des limites / défauts métier, au mécanisme que j’avais mis en place.

Proposer un thème light et un thème dark, à la convenance de l’utilisateur, est devenu une fonctionnalité basique. De plus en plus, les sites offrent une troisième option à leurs visiteurs consistant à synchroniser le thème automatiquement avec le thème système.

J’ai bien pris en compte cette fonctionnalité dans mon article. Cependant, d’un point de vue ergonomie, la solution retenue - un bouton switch, avec un comportement par défaut s’il n’a “jamais” été cliqué - présente plusieurs défauts :

  • elle est irréversible
  • sauf à aller vider manuellement le Local Storage (bon courage sous iPhone ou Android)
  • sans que l’utilisateur en ait conscience ou ne puisse faire quelque chose

Par ailleurs, en termes d’accessibilité, là aussi j’ai fait des efforts et la solution proposée est satisfaisante, mais il y des soucis au niveau du rendu graphique en cas de focus (absence de bordure) et l’utilisation des aria-* n’étaient pas dingue.

Bref, c’était très loin d’être optimal et j’ai décidé de revoir ma copie.

Exemples

Dans les retours importants qui m’ont été faits (merci à tous pour les feedbacks 🙏), j’ai notamment eu celui de Vincent (a.k.a. “La Relève”) qui m’a pointé l’exemple de Mozilla et son site MDN Web Docs.

Au moment où j’écris ces lignes, le menu de sélection du thème de mon site est “très fortement inspiré” du leur.

(*) en vrai, j’ai quasi tout pompé, jusqu’à la structure HTML et la gestion de l’accessibilité 😬

MDN Web Docs :

Sélecteur de thème pour le site developers.mozilla.com

Ci-dessous, je vous partage des captures de plusieurs autres sites qui proposent eux-aussi une gestion moderne de choix du thème de couleurs.

GitHub :

Sélecteur de thème pour le site github.com

StackOverflow :

Sélecteur de thème pour le site stackoverflow.com

Même un projet de plus petite envergure comme Plausible propose un menu de sélection du thème proposant le choix système plutôt qu’un simple bouton toggle.

Plausible :

Sélecteur de thème pour l’application Web Plausible

Conception

En termes d’attentes, l’objectif est d’avoir exactement le même comportement que le site de Mozilla :

Visuel

  • un élément interactif pour ouvrir le menu des thèmes
    • en l’occurrence, un simple bouton
    • situé dans la barre d’en-tête du site sur toutes les pages, en lieu et place du bouton à bascule initial
  • une signalétique / des éléments pour visualiser en un coup le thème actuel
    • une icône dans la barre d’en-tête correspondant au thème sélectionné, associé au bouton déroulant
    • un style particulier pour l’option de thème actuel, dans le menu déroulant
  • un menu déroulant avec les 3 options possibles :
    • “os-default”, pour synchroniser le thème du site avec les préférences utilisateurs
    • “light”, pour forcer le mode clair
    • “dark”, pour forcer le mode sombre
    • une icône spécifique pour chaque option (reprise dans l’icône de la barre d’en-tête du site, cf. ci-dessus)

Comportements

  • quand on clique sur le “bouton de sélection du thème”, alors
    • le menu déroulant (menu popup) apparaît,
    • présentant les 3 options de thème
    • avec une visualisation particulière pour le thème actuel
  • quand on sélectionne une option :
    • dans tous les cas le menu se ferme et l’icône dans la barre d’en-tête est mise à jour, par rapport au thème sélectionné
    • si le thème sélectionné est celui actuel, rien d’autre ne se passe
    • sinon, l’icône du menu principal change et lorsque l’on ouvre à nouveau le menu déroulant, l’item sélectionné est bien celui précédemment sélectionné
  • lorsque le menu déroulant est ouvert, il est possible de le fermer sans effet :
    • en cliquant en dehors du menu
    • en appuyant sur la touche d’échappement

Autres

  • par défaut, le thème sélectionné est celui des préférences système
  • lorsque l’utilisateur revient sur le site, ses préférences de navigation sont restaurées / chargées
  • le menu doit être accessible :
    • il doit être possible de naviguer, ouvrir et fermer le menu au clavier
    • les outils de lecture d’écran doivent fonctionner correctement
    • le menu doit satisfaire aux exigences de WAVE + LightHouse
  • ce type de composant ne doit pas dégrader les performances du site

Réalisation

Général

Je prends le parti d’avoir une solution la plus générique et agnostique possible. Intention ou idée : pouvoir simplement componentiser (dans un Web Component) le résultat final.

Ainsi, tous les event listeners / handlers seront déclarés directement dans un fichier JS dédié : theme-switcher-menu.js (dans le répertoire des assets).

Proposer un menu déroulant (plutôt qu’un bouton à bascule)

De base, au chargement de la page, le menu déroulant est déclaré dans le HTML/DOM, mais caché (via la classe .hidden).

Celui-ci est matérialisé sous la forme d’une liste non ordonnée (élément <ul>) avec des boutons.

🤔 J’ai repris la structure proposée par Mozilla. J’avoue que je m’attendais plutôt à un élément <select>.

L’ouverture du menu passe par un élément de type <button>. On précise l’attribut type="button" pour éviter l’envoi éventuel de formulaire et s’épargner une instruction event.preventDefault() dans le code.

On branche un écouteur sur l’évènement click du bouton.

  • si le menu - <ul.theme-switcher-menu__list> - est caché (possède la classe .hidden), alors on l’affiche (on supprime la classe)
  • sinon, on le ferme (ajout de la classe sur l’élément <ul>)
const themeSwitcherMenuToggle = document.querySelector('.theme-switcher-menu__toggle');
const themeSwitcherMenuList = document.querySelector('.theme-switcher-menu__list');

themeSwitcherMenuToggle.addEventListener('click', () => {
  if (themeSwitcherMenuList.classList.contains('hidden')) {
    themeSwitcherMenuList.classList.remove('hidden');
  } else {
    themeSwitcherMenuList.classList.add('hidden');
  }
});

ℹ️ L’implémentation est un peu différente ici de ce que fait Mozilla, qui utilise React et donc passe par les mécaniques liées au Shadow DOM.

Tenir compte du thème sélectionné

À la différence du bouton à bascule simple, détecter l’option choisie par l’utilisateur nécessite de passer par un moyen quelconque. J’ai opté pour un attribut de donnée data-theme-option dont la valeur dépende de l’option.

À l’initialisation de la page / exécution du script, l’idée consiste à associer un event handler pour chaque évènement click des 3 boutons-options possible.

const themeSwitchMenuButtons = document.querySelectorAll('.theme-switcher-menu__button');

themeSwitchMenuButtons.forEach((element) => {
  element.addEventListener('click', () => {
    themeSwitcherMenuList.classList.add('hidden');

    if (element.getAttribute('data-theme-option') !== getCurrentMode()) {
      const targetMode = element.getAttribute('data-theme-option');
      if (targetMode === 'os-default') {
        localStorage.removeItem('theme');
        initTheme();
      } else {
        localStorage.setItem('theme', targetMode);
        setTheme(targetMode);
        setToggleIcon(targetMode);
        setActiveOption(targetMode);
      }
    }
  });
});

ℹ️ Je ne rentre pas ici dans le détail de la gestion des préférences système utilisateur via la Media Query window.matchMedia("(prefers-color-scheme: dark)") ou de la persistance du choix cross-visites via le Local Storage. Tout est dans l’article précédent.

Tenir compte de la navigation au clavier

L’élément qui affiche / cache le menu déroulant, ainsi que les 3 options du menu sont tous des boutons. De fait, en tant que boutons