Ajouter un sélecteur de thème à un site fait avec Bulma
Le framework CSS Bulma, utilisé sur ce site, est enfin passé cette année en version 1.0, et parmi les nouveautés de cette version, la plus importante est de permettre un affichage en mode sombre.
J'ai finalement pris le temps de mettre cela en place, et je me suis dit que ça méritait un petit point sur l'ajout du sélecteur qui permet de passer facilement d’un thème à l'autre, puisque Bulma lui-même n’en fournit pas.
En effet, je voulais que par défaut cela suive le thème du navigateur des visiteurs, mais en laissant la possibilité de choisir manuellement le thème parmi les deux proposés.
Prise en compte du thème
Comme indiqué dans la documentation, il suffit d'ajouter l'attribut data-theme
à la balise <html>
principale du site.
<!-- templates/_base.html --> <!DOCTYPE html> <html data-theme="{{ request.COOKIES.theme|default:'system' }}" lang="{{ LANGUAGE_CODE }}"> <!-- [...] --> </html>
La balise ajoutée fait ici deux choses : vérifier si le cookie theme
, que je vais créer pour conserver le choix de l’utilisateur, existe, auquel cas l’attribut data-theme
se verra attribuer sa valeur, ou si ce n’est pas le cas, mettre par défaut la valeur system
pour que le thème du navigateur soit utilisée à la place[1]
.
Le sélecteur lui-même
Il faut ensuite ajouter le sélecteur au menu d’en-tête du site.
<!-- templates/blocks/header.html --> {% load i18n %} <ul class="navbar-end"> <li class="navbar-item has-dropdown is-hoverable"> <a class="navbar-link" href="#"> <span class="icon-text"> <span class="icon"> <i id="theme-selector-icon" class="{% if request.COOKIES.theme == "dark" %}ri-moon-fill{% else %}ri-sun-fill{% endif %}" aria-hidden="true"></i> </span> <span class="is-sr-only">{% translate "Choose color scheme" %}</span> </span> </a> <div id="theme-selector" class="navbar-dropdown is-right"> <a class="navbar-item{% if request.COOKIES.theme == "light" %} is-active{% endif %}" href="#" data-theme="light"> <span class="icon"> <i class="ri-sun-line" aria-hidden="true"></i> </span> <span>{% translate "Light" %}</span> </a> <a class="navbar-item{% if request.COOKIES.theme == "dark" %} is-active{% endif %}" href="#" data-theme="dark"> <span class="icon"> <i class="ri-moon-line" aria-hidden="true"></i> </span> <span>{% translate "Dark" %}</span> </a> <a class="navbar-item{% if not request.COOKIES.theme == "light" and not request.COOKIES.theme == "dark" %} is-active{% endif %}" href="#" data-theme="system"> <span class="icon"> <i class="ri-computer-line" aria-hidden="true"></i> </span> <span>{% translate "System" %}</span> </a> </div> </li>
Il s'agit d’un menu déroulant avec trois options. Lorsqu'il est enroulé, le lien principal affiche juste une icône, dont l’id theme-selector-icon
va permettre de la contrôler lorsqu'on change le thème. Par défaut, à moins que le thème sombre soit utilisé, elle affiche une icône de soleil. Elle a aussi un libellé clair dans un <span class="is-sr-only">{% translate "Choose color scheme" %}</span>
pour l’accessibilité.
Les trois options sont des liens dont on vérifie s'ils sont actifs par rapport au cookie theme
, et dont on passe la nouvelle valeur au script qui contrôle le comportement via l’attribut data-theme
.
Le script
Il ne reste plus qu'à créer un script qui contrôle tout cela.
function setCookie(key, value) { const expiration_date = new Date(); expiration_date.setTime(expiration_date.getTime() + (365 * 24 * 60 * 60 * 1000)); let expires = expiration_date.toUTCString(); let hostname = window.location.hostname; if (hostname.includes(".")) { // Only keep the main domain level let hostname_parts = hostname.split(".").splice(-2); hostname = "." + hostname_parts.join(".") } document.cookie = key + "=" + value + ";expires=" + expires + ";path=/" + "; domain=" + hostname + ";SameSite=Lax"; } document.addEventListener('DOMContentLoaded', () => { // Manage theme selector const themeSelectors = Array.prototype.slice.call(document.querySelectorAll("#theme-selector a"), 0); const themeSelectorIcon = document.getElementById("theme-selector-icon"); themeSelectors.forEach(el => { el.addEventListener('click', event => { event.preventDefault(); const theme = el.dataset.theme; document.documentElement.setAttribute('data-theme', theme); setCookie("theme", theme); themeSelectors.forEach(el => { el.classList.remove('is-active'); }) el.classList.add('is-active'); if (theme == "dark") { themeSelectorIcon.className = "ri-moon-fill"; } else { themeSelectorIcon.className = "ri-sun-fill"; } }); }); });
Lorsque la page est chargée, on récupère les trois liens comportant les options du sélecteur dans la variable themeSelectors
, et pour chacun d’entre eux on ajoute un listener qui réagit à la présence d’un clic[2]
, et qui dans ce cas :
- empêche le fonctionnement par défaut du clic (
event.preventDefault();
) - récupère l’identifiant du thème depuis l'attribut
data-theme
défini plus haut. - place cet identifiant comme valeur de
data-theme
pour l’élément<html>
racine (document.documentElement
) - le place également dans le cookie theme, grâce à une fonction setCookie qui définit un cookie valable un an, sur l'ensemble pages de tous les sous-domaines du domaine .ash.bzh (ou .boissel.dev),
- retire la classe
is-active
sur tous les liens du sélecteur et l’ajoute sur celui qui vient d’être cliqué - change l’icône du menu pour refléter le thème choisi.
Et... C'est tout. Ces trois petits bouts de code suffisent pour gérer le choix du thème, le communiquer entre le front-end et le côté serveur, et le communiquer d’un sous-domaine à l’autre.
Image d’en-tête:
Vitrail du Quatrième Jour de la Création, Cathédrale d'York (détail). Anonyme, domaine public.