On développe un menu déroulant pour permettre à vos visiteurs de consulter et d'accéder facilement à toutes les pages de votre site Web.

Le menu est réalisé uniquement HTML et CSS dans un premier temps, pour des améliorations sont ajoutées en utilisant JavaScript.

Regarder la vidéo sur YouTube
Voir une démo du résultat final
Voir le code source

Si vous ne pouvez pas regarder la vidéo, un compte rendu est proposé plus bas.

Développer un menu déroulant

Dans un premier temps, l'objectif est de créer le menu uniquement en HTML et CSS, sans utiliser de JavaScript. Cela permet de rendre le menu fonctionnel rapidement, dès que le navigateur du visiteur reçoit le contenu HTML de notre page, et sans attendre le téléchargement de ressources extérieures, comme des librairies ou des programmes JavaScript.

Par la suite, on utilisera quelques lignes de JavaScript pour améliorer l'expérience de l'utilisateur en rajouter des fonctionnalités qui ne sont pas possibles en HTML.

Pour ouvrir le menu, l'utilisateur devra cliquer sur une image composée de trois barres verticales, appelée menu hamburger, comme cela se fait habituellement :

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="#666">
  <rect x="0" y="4" rx="3" ry="3" width="32" height="3" />
  <rect x="0" y="14" rx="3" ry="3" width="32" height="3" />
  <rect x="0" y="24" rx="3" ry="3" width="32" height="3" />
</svg>

L'image qu'on va utiliser est une image vectorielle, au format SVG, qui a l'avantage :

  • d'être affichée en haute qualité, contrairement aux images classiques matricielles, comme PNG ou JPEG, qui sont composées de pixels.
  • de pouvoir être intégrée directement à un document HTML, et ne pas demander à l'utilisateur de télécharger une ressource extérieure.

Cette image peut être ajoutée directement à notre page HTML :

<body>
  <svg viewBox="0 0 32 32" fill="#666">
    <rect x="0" y="4" rx="3" ry="3" width="32" height="3" />
    <rect x="0" y="14" rx="3" ry="3" width="32" height="3" />
    <rect x="0" y="24" rx="3" ry="3" width="32" height="3" />
  </svg>
</body>

On peut aussi ajouter quelques lignes de CSS pour positionner ce menu en haut à droite de la page :

svg {
  /* Position figée, indépendant du 
     défilement de la page */
  position: fixed;
  top: 20px;
  right: 30px;

  /* Taille fixe */
  height: 32px;
  width: 32px;
}

Pour conserver l'état du menu, c'est-à-dire ouvert ou fermé, on va utiliser une case à cocher (checkbox). Cette case sera cochée pour indiquer que le menu est ouvert et décochée dans le cas contraire.

<!-- Case à cocher -->
<input type="checkbox" id="menu-cb" class="menu-cb">

Cette case peut être cochée et décochée de façon traditionnelle, en cliquant dessus. Elle peut aussi être pilotée en cliquant un élément <label>. On va donc ajouter un élément label et y intégrer notre image de menu :

<!-- Label associé à la case. -->
<label for="menu-cb" class="menu-label">
  <!-- Image du menu -->
  <svg viewBox="0 0 32 32" fill="#666">
    <rect x="0" y="4" rx="3" ry="3" width="32" height="3" />
    <rect x="0" y="14" rx="3" ry="3" width="32" height="3" />
    <rect x="0" y="24" rx="3" ry="3" width="32" height="3" />
  </svg>
</label>
<!-- Case à cocher -->
<input type="checkbox" id="menu-cb" class="menu-cb">

Il est important de noter que l'attribut for de l'élément label doit contenir l'identifiant de l'élément associé, dans notre case l'identifiant de notre case à cocher.

On peut désormais piloter notre case en cliquant sur l'image du menu, et avec un peu de CSS, on peut faire en sorte que cette case ne soit pas visible à l'écran de l'utilisateur :

/* Associe les mêmes règles au label et à la case
   pour les superposer, et "cacher" la case à cocher. */
.menu-label,
.menu-cb {
  /* Retire l'affichage par défaut des éléments de formulaire */
  /* ... comme les checkbox */
  appearance: none;

  /* Défini une position fixe, comme ajouté précedemment */
  position: fixed;
  height: 32px;
  width: 32px;
  top: 20px;
  right: 30px;
}

On peut, à présent, associé l'état de cette case à la fenêtre contenant les liens de notre menu.

La fenêtre à afficher sera la suivante :

<!-- [...label...checkbox...] -->
<nav class="menu-nav">
  <ul>
    <li class="menu-item"><a href="#page1">Page 1</a></li>
    <li class="menu-item"><a href="#page2">Page 2</a></li>
    <li class="menu-item"><a href="#page3">Page 3</a></li>
    <li class="menu-item"><a href="#page3">Page 4</a></li>
  </ul>
</nav>

Le contenu de la fenêtre peut, bien sûr, être personnalisé, que ce soit au niveau son contenu ou de son style. Ce qui est important, c'est qu'elle soit affichée en fonction de l'état de notre case à cocher.

Initialement, cette fenêtre doit être cachée, c'est-à-dire qu'elle doit comporter la règle CSS :

.menu-nav {
  /* [...les autres propriétés CSS...] */
  display: none;
}

Lorsque la case est cochée, cet attribut est modifié en utilisant le sélecteur :checked de la checkbox :

/* SI la case est cochée, modifier la classe "menu-nav" */
.menu-cb:checked ~ .menu-nav {
  display: block;
}

À présent, on peut piloter notre fenêtre en cliquant sur l'image de menu. Selon la disposition de votre fenêtre, il est possible que celle-ci cache l'icône du menu, rendant impossible à l'utilisateur de re-cliquer dessus et donc de fermer la fenêtre. Dans ce cas, il est possible d'ajouter la règle z-index et d'y associer un nombre. L'élément avec le nombre z-index le plus grand sera affiché en priorité :

.menu-label,
.menu-cb {
  /* [...] */
  z-index: 10;
}

Optimiser l'affichage de la fenêtre

Lorsque la case est cochée, le fenêtre s'affiche, car la propriété display passe de none à block.

Cette façon d'afficher la fenêtre n'est pas optimale, car elle demande au navigateur de recalculer les éléments de la page. Lorsque le navigateur doit régénérer les positions des éléments de la page, cela a un impact négatif pour l'utilisateur, car le navigateur consommera plus de mémoire et utilisera davantage la batterie de l'utilisateur. Le site csstriggers.com liste les propriétés qui nécéssitent un recalcul partiel ou total de la page.

En utilisant transform plutôt que display, on peut améliorer ce comportement, car transform ne nécessite pas un recalcul global de la page :

.menu-nav {
  /* A la place de "display: none" :
     translater sur l'axe X, càd décaler horizontalement
     l'élement pour le sortir de la page */
  transform: translateX(100%);
}

.menu-cb:checked ~ .menu-nav {
  /* A la place de "display: block" :
     Remettre l'élément à sa place d'origine */
  transform: translateX(0);
}

Améliorer la cloture du menu en JavaScript

En utilisant uniquement du HTML et du CSS, il n'est pas possible d'offrir la possibilité à l'utilisateur de fermer le menu en cliquant ailleurs sur la page. C'est pourtant un comportement assez naturel des internautes. On peut ajouter cette fonctionnalité en rajoutant un peu de JavaScript.

L'idée est d'ajouter un élément qui couvre l'ensemble la page et l'afficher lorsque le menu est ouvert. On peut alors détecter lorsque le visiteur clique sur la page, en dehors du menu, et fermer le menu :

const fermerMenu = () => {
  // Récupérer la case à cocher
  const input = document.getElementById('menu-cb')
  // Changer son état
  // .. ce qui a pour conséquence de le retirer de l'écran
  input.checked = false

  // Supprimer l'élément transparent ajouté à l'ouverture du menu
  const fenetreNode = document.getElementById('menu-cote')
  fenetreNode.remove()
}

const changerEtatMenu = () => {
  // Récupérer la case à cocher
  const input = document.getElementById('menu-cb')
  // Récupérer l'état de la case
  const actif = input.checked

  // Si le menu est affiché
  if (actif) {
    // Ajouter un élément transparent
    const fenetreNode = document.createElement('div')
    fenetreNode.id = 'menu-cote'
    fenetreNode.className = 'menu-cote'

    // Ecouter lorsque le visiteur clique dessus
    fenetreNode.addEventListener('click', fermerMenu)
    
    // Ajouter l'élément à la page
    document.body.appendChild(fenetreNode)
  } else {
    // ... sinon ...
    // Supprimer l'élément fictif ajouté à l'ouverture du menu
    const fenetreNode = document.getElementById('menu-cote')
    fenetreNode.remove()
  }
}

// Ecouter lorsque le visiteur clique sur le menu
const input = document.getElementById('menu-cb')
input.addEventListener('click', changerEtatMenu)

Cet élément qui couvre l'ensemble de la page a les règles CSS suivantes :

.menu-cote {
  position: fixed;
  top: 0;
  left: 0;
  height: 100%;
  width: 100%;
}

Pour aller plus loin (1) - Style

Il est possible de faire en sorte que le menu s'affiche, non pas de la droite, mais du haut de l'écran, du bas, ou de la gauche en changeant la propriété translateX.

Il est aussi possible de modifier la façon dont la fenêtre s'affiche en utilisant la propriété transition. On peut, par exemple, l'afficher progressivement sur une durée de 1 seconde :

.menu-nav {
  transform: translateX(100%);
  transition: transform 1s linear;
}

Pour aller plus loin (2) - Sous menu

Il est possible d'appliquer le même concept pour ajouter un sous menu, qui peut être affiché lorsque l'utilisateur le souhaite.

Voir une démo d'un menu avec un sous menu
Voir le code source

Pour aller plus loin (3) - Fermeture du menu après rafraichissement de la page

Lorsque l'utilisateur ouvre le menu et rafraichit la page, il n'est pas possible de le fermer en cliquant sur la page.

Ces quelques lignes de code permettent de prendre en compte cette éventualité :

// Déjà présent
const input = document.getElementById('menu-cb')
input.addEventListener('click', changerEtatMenu)

// Ajout :
// Lorsque le menu a été déroulé et l'utilisateur rafraichit
// la page, exécuter la procédure d'ouverture pour permettre à
// l'utilisateur de clôre le menu en cliquant sur la page
if (input.checked) {
  changerEtatMenu()
}

MAJ du 21/10/2021 : ajout de l'exemple avec un sous menu.
MAJ du 18/02/2022 : ajout de la possibilité de fermer le menu après le rafraichissement de la page