Ajouter un sélecteur de thème à un site fait avec Bulma

Ash_Crow Webdesign

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.

Mots-clefs :

0 commentaire publié.

Notes de bas de page

  1. Cela marchera en fait pour n’importe quelle autre valeur que light ou dark.

  2. Ou d’un appui sur Entrée lors de la navigation au clavier.

Commentaires

Ajouter un nouveau commentaire

Required for comment verification