Comment faire un menu responsive ? (Une bonne fois pour toutes)

Avez-vous déjà ressenti cela ?

Vous êtes en face d’un design à intégrer, vous le découpez en sous partie, vous décidez de commencer par faire le menu de navigation (ce qui se fait souvent en même temps que le header), puis les éléments communs aux pages (sidebar, footer, etc.) et enfin finir sur ce qui est unique.

Un schéma d’intégration plutôt courant.

Seulement voilà, vous vous sentez ennuyé car vous savez bien que vous allez (encore) vous répéter…

Faire un menu de navigation c’est à peu près toujours pareil.

90% des interfaces en ont un, voir plusieurs, l’apparence varie, mais la base est toujours la même :

  • un logo et / ou un titre ;
  • une liste de liens ;
  • et parfois, une barre de recherche.

De plus, le téléphone étant le terminal le plus utilisé pour surfer, on ne peut pas se permettre de faire un menu qui ne soit pas responsive.

Il faut prévoir au moins 2 affichages et, là encore, ça se répète. C’est connu d’avance, les affichages à faire seront : le menu aux formats horizontal et vertical. 

Un classique qui ressemble à peu près à ça :

Le problème d’un tel menu

Par rapport au reste de l’interface, ça prend du temps à faire.

Et, pour quelque chose d’aussi chronophage, répétitif et évident, ce serait bien qu’à chaque projet ce soit déjà fait (ou au moins à moitié) !

La solution est dans le guide pas à pas

Une première solution serait d’utiliser un framework CSS, mais ça ne convient pas à l’intégration d’un design spécifique

L’autre est de lire la suite de cet article, je vous montre comment faire un menu responsive et moderne.

Ce que nous allons construire ensemble pourra être réutilisé dans beaucoup d’autres projets.

(Bien entendu il s’agit de faire un menu avec des micro-animations)

Guide pas à pas : mise en place du projet

Commençons par une organisation assez classique : on a un fichier index.html et un dossier assets où sont rangés les fichiers de style style.css et de script app.js.

Le markup HTML

Notre page HTML5, importe le stylesheet, notre script et une font family (Roboto) dans la section “head”.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Simple Responsive Menu</title>
 
    <link rel="stylesheet" href="./assets/style.css">
    <script src="./assets/app.js" defer></script>
 
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
  </head>
  <body> 
    <header class="header">
      <nav class="container flex-wrapper">
        <a class="brand-name" href="/">
            Simple Responsive Menu
        </a>
        <ul class="menu">
            <li class="item"><a href="#" class="link">Articles</a></li>
            <li class="item"><a href="#" class="link">About us</a></li>
            <li class="item"><a href="#" class="link">Newsletter</a></li>
        </ul>
        <div class="burger">
            <div class="line"></div>
            <div class="line"></div>
            <div class="line"></div>
        </div>
      </nav>
    </header>
    <main>
	...
    </main>
  </body>
</html>

Dans le “body”, on crée l’entête de notre page “header”.

Et, puisqu’on cherche à regrouper une série d’éléments destinés à la navigation, on va créer sous notre “header” une balise nav.

Dans cette dernière, on regroupe un lien “.brand-name”, une liste de liens de navigation “.menu” et une div “.burger” qui servira (plus tard) à faire notre menu burger.

Tout ce qui est à l’intérieur de “main” est du bonus non utile au menu, mais il vous sera utile pour faire des pages en un éclair…

Le code du fichier style

Les règles de style sont basiques, on commence par enlever les marges (par défaut) des éléments de la page, puis on initialise quelques variables.

Il y a beaucoup de lignes qui servent à régler les tailles de polices, les couleurs et les espaces. On ne va pas s’attarder dessus, j’explique à partir de ce qui vient sous le commentaire : “La barre de navigation commence ici”.

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
 
:root {
  --primary: 169deg 53% 14%;
  --dark: hsl(169deg 53% 3%);
  --light: hsl(169deg 53% 97%);
 
  --header-container-height: 10vh;
  --header-menu-right-margin: 25px;
 
  --font-size-normal: 1em;
  --font-size-medium: 1.12em;
  --font-size-large: 1.34em;
  --font-size-xlarge: 1.7em;
  --font-size-xxlarge: 2.1em;
  --transition-time: 0.3s;
}
 
::selection {
  background-color: hsl(var(--primary) / 30%);
}
 
body {
  font-family: "Roboto", sans-serif;
  font-weight: 400;
  /* 375 x 812 the font size will be ~ 16.125px*/
  font-size: min(4.3vw, 18px);
  color: var(--dark);
  background: var(--light);
  line-height: 1.4;
}
 
h1 {font-size: var( --font-size-xxlarge); }
h2 { font-size: var( --font-size-xlarge); }
h3 { font-size: var( --font-size-large); }
h4 { font-size: var( --font-size-medium); }
h1, h2, h3, h4, p { margin-bottom: 1em; }
 
a {
  text-underline-offset: 0.2em;
  color: var(--dark);
}
 
a:hover {
 text-decoration-style: dotted;
}
 
/* La barre de navigation commence ici ! */
.container {
  width: 80%;
  max-width: 1440px;
  margin: 0 auto;
}
 
.flex-wrapper {
  display: flex;
  align-items: center;
}
 
.header .flex-wrapper {
  justify-content: space-between;
  height: var(--header-container-height);
}
 
.brand-name {
  font-size: var(--font-size-medium);
  text-decoration-line: none;
  font-weight: 700;
}
 
.header {
  box-shadow: 0 10px 38px hsl(var(--primary) / 15%);
}
 
.menu {
  list-style: none;
  text-decoration: none;
}
 
.menu .link {
  text-decoration: none;
}
 
.menu .link:hover {
  text-decoration: underline;
  text-decoration-style: dotted;
}
 
.header .menu .link {
  font-weight: 500;
}
 
.burger {
  cursor: pointer;
}
 
.burger .line {
  background: var(--dark);
  width: 2em;
  height: 0.18em;
}
 
.burger .line:nth-child(2) {
  margin: 6px 0;
}
 
.content .container {
  padding: var(--font-size-xxlarge) 0;
}

La barre de navigation

Les éléments de navigation sont regroupés et cadrés sur la page avec les classes “.container” et “.flex-wrapper”.

On espace les éléments (“.brand-name”, “.menu” et “.burger”) présent dans “.header”, puis on donne à notre barre de navigation la hauteur de 10vh.

.container {
   padding: 0 var(--header-menu-right-margin);
}
 
.flex-wrapper {
  display: flex;
  align-items: center;
}
 
.header .flex-wrapper {
  justify-content: space-between;
  height: var(--header-container-height);
}

On style un peu notre menu en :

  • ajustant les polices,
  • ajoutant une ombre sous le header pour qu’il se distingue du reste de la page,
  • enlevant les points des listes à puces,
  • précisant comment s’affichent les liens au survol du curseur,
.brand-name {
  font-size: var(--font-size-medium);
  text-decoration-line: none;
  font-weight: 700;
}
 
.header {
  box-shadow: 0 10px 38px hsl(var(--primary) / 15%);
}
 
.menu {
  list-style: none;
  text-decoration: none;
}
 
.menu .link {
  text-decoration: none;
}
 
.menu .link:hover {
  text-decoration: underline;
  text-decoration-style: dotted;
}

Le menu burger

Pour réaliser l’icône de menu “burger”, on se sert des 3 lignes .burger .ligne pour faire des traits et on utilise celle du milieu “.burger .line:nth-child(2)”, pour créer l’espace entre les 3.

.burger .line {
  background: var(--dark);
  width: 2em;
  height: 0.18em;
}
 
.burger .line:nth-child(2) {
  margin: 6px 0;
}

A ce stade vous devriez avoir une barre de navigation comme ci-dessous :

Présentation verticale

“Une bonne habitude à prendre est de commencer par faire l’affichage pour mobile.”

Et c’est que l’on va faire !

On veut que notre menu soit toujours disponible (même lors du scrolling), qu’il soit fixé en haut à droite, qu’il fasse 100% de hauteur et 66% de largeur du viewport.

.header .menu {
  position: fixed;
  top: 0;
  right: 0;
 
  height: 100vh;
  width: 66vw;
 
  text-align: right;
  padding-top: var(--header-container-height);
  padding-right: var(--header-menu-right-margin);
  background: hsl(var(--primary) / 95%);
}

On fait de même avec “l’icône” du menu burger

.burger {
  position: fixed; 
  right: var(--header-menu-right-margin);
  cursor: pointer;
}

Votre menu devrait ressembler à ça :

Seulement, par défaut il n’est pas ouvert et on ne le voit pas. Pour le cacher, on le pousse hors de l’écran (en tout cas ce qui est visible).

À la suite de ce qu’il y a déjà dans la règle .header .menu {}, il faut ajouter une translation dans une zone non visible :

.header .menu {
  ...
  transform: translateX(100%);
}

Comment contrôler l’ouverture de notre menu

On va se servir d’une classe « .open » et d’un peu de Javascript.

La class .open

.header .menu.open {
  transform: translateX(0);
  transition: transform var(--transition-time) ease-out;
}

La nouvelle classe va annuler l’état dans lequel on avait mis le menu et sera insérée (automatiquement) dans le markup HTML lorsque l’on clique sur le menu burger.

 <ul class="menu open">
  ...
</ul>

L’ajout automatique de la classe .open

Dans app.js on initialise 2 constantes, une qui correspond au bouton  “.burger” (hé oui, maintenant il va remplir cette fonction) et une autre au menu de notre entête.

Puis, sur notre bouton “.burger”, on attache une action à effectuer lorsque l’on clique dessus. Celle-ci consiste à ajouter ou enlever la classe “open” de notre menu en fonction de son état.

(On fait de même avec notre bouton burger).

const burger = document.querySelector(".burger")
const headerMenu = document.querySelector(".header .menu")
 
burger.addEventListener("click", () => {
    headerMenu.classList.toggle("open")
    burger.classList.toggle("open")
})

Hé tada !

Présentation et animation de notre menu ouvert

Commençons par changer les couleurs des liens.

.header .menu.open .link {
  color: var(--light);
  font-size: var(--font-size-large);
}

Puis, la couleur des lignes de notre bouton d’ouverture de menu.

.burger.open .line {
  background: var(--light);
}

Vous remarquerez que l’on sélectionne ces 2 éléments avec la classe “.open”.

Votre menu devrait ressembler à ça :

Parce qu’on souhaite conserver l’habitude de cliquer sur la croix pour fermer une fenêtre ou une pop-up ou un menu, nous allons donner la forme d’une croix à notre bouton burger ouvert.

Avant
Après

La croix est composée de 2 barres, on cache celle du milieu et on pivote de 45 degrés les 2 autres.

(Si vous ne saisissez pas bien la sélection des lignes, n’hésitez pas à consulter la documentation de la pseudo class :nth-child()).

.burger.open .line:nth-child(1) {
  transform: rotate(45deg) translate(10%, 5%);
}
 
.burger.open .line:nth-child(2) {
  display: none;
}
 
.burger.open .line:nth-child(3) {
  transform: rotate(-45deg) translate(5%, 10%);
}

L’animation se fera en ajoutant une transition à la règle  .burger.open .line {}. 

.burger.open .line {
  background: var(--light);
  transition: transform calc(var(--transition-time) * 2) ease-out;
}

Présentation horizontale

J’ai choisi de la faire lorsque l’écran dépasse les tailles de 640px et pour cela il faut utiliser une media query

@media screen and (min-width: 640px) {}

Dans notre media query on commence par cacher le bouton du menu.

@media screen and (min-width: 640px) { 
  .burger {
    display: none;
  }

Puis on réinitialise la position de notre menu, par la même occasion on affiche le menu en display flex pour qu’il soit à l’horizontal.

.header .menu {
    position: initial;
    text-align: initial;
    padding: initial;
    height: initial;
    width: initial;
    background: initial;
    transform: initial;

    display: flex;
    gap: 0.8em;
  }

Enfin on (ré)ajuste les marges de notre “.container”

  .container {
     width: 80%;
     max-width: 1440px;
     margin: 0 auto;
     padding: initial;
  }

Bonus : animations supplémentaires d’ouverture de menu

On pourrait s’arrêter là, le menu est responsive, il se déplace en même temps que l’on scroll et est plutôt discret…

Mais, ce serait bien qu’il fasse une entrée un peu plus remarquable.

On va ajouter une micro-animation supplémentaire.

Pour rappel, il faut que le temps d’exécution ne soit pas trop long. L’animation ne doit pas non plus être contradictoire à l’action en cours.

Ce que l’on va faire c’est suivre le mouvement de translation lors de l’ouverture du menu, par contre, on va retarder l’arrivée des éléments du menu “.item”. Ces derniers arriveront avec un décalage dans le temps et le tout ne prendra pas plus de 800 ms.

Comme pour le menu, on commence par sortir les .item” de l’écran.

.header .menu .item {
  transform: translateX(100%);
}

Vu que ce ne sera valable que sur mobile, pensez à annuler cette règle sur l’affichage horizontal.

@media screen and (min-width: 640px) {   
  .header .menu .item {
    transform: initial;
  }
}

Une fois le menu ouvert, on anime l’arrivée des éléments du menu et par conséquent les liens.

.header .menu.open .item {
  animation: mobileMenuFadeIn calc(var(--transition-time) * 2) forwards;
}

“mobileMenuFadeIn” est une animation personnalisée ;  “forwards”, permet de conserver le style de la fin de notre animation.

@keyframes mobileMenuFadeIn {
  0% {
    opacity: 0;
  }
 
  70% {
    opacity: 0.5;
  }
 
  100% {
    transform: translateX(0);
    opacity: 1;
  }
}

Le résultat est intéressant, mais nous sommes dans une étape intermédiaire :

À présent, on va décaler l’arrivée des “.item”.  Pour cela il faut ajouter un délai (animation-delay) sur l’animation de chaque élément. Toutefois, on ne sait pas à l’avance combien d’éléments il y aura.

Nous allons “automatiser” et par là, il faut comprendre : scripter cette tâche avec du JS.

const burger = document.querySelector(".burger")
const headerMenu = document.querySelector(".header .menu")
 
/** nouvelle ligne */
const headerMenuItems = document.querySelectorAll(".header .menu .item")
 
burger.addEventListener("click", () => {
    headerMenu.classList.toggle("open")
    burger.classList.toggle("open")
    
    /** nouvelles lignes */
    headerMenuItems.forEach((item, index) => {
        if(item.style.animationDelay)
            return item.style.animationDelay = "";
           
        const maxDelay = 0.8
        const delay = index / 12
        const animationDelay = delay >= maxDelay ? maxDelay : delay
        
        return item.style.animationDelay = `${animationDelay}s`        
    })
})

On commence par sélectionner (tous) nos « .item »,

/** nouvelle ligne */
const headerMenuItems = document.querySelectorAll(".header .menu .item")

puis après le clic sur le bouton d’ouverture de menu,

burger.addEventListener("click", …

on va boucler sur chaque élément et leur ajouter un délai en fonction de leur ordre dans le menu. Ce délai n’excède pas 800ms.

Cependant, si l’élément en question à déjà un délai, on le supprime ; ce sera le cas lorsque l’on va refermer le menu…

(Unreturn” dans une boucle forEach, fait passer à l’itération suivante).

headerMenuItems.forEach((item, index) => {
    if(item.style.animationDelay)
       return item.style.animationDelay = "";
           
    const maxDelay = 0.8
    const delay = index / 12
    const animationDelay = delay >= maxDelay ? maxDelay : delay
        
    return item.style.animationDelay = `${animationDelay}s`        
 })

Et on obtient notre résultat final :

Le bout du tunnel

Merci de m’avoir lu jusqu’au bout !

J’espère que cette ressource vous sera utile pour vos (futurs) projets.

Si vous avez aimé, n’hésitez pas à partager cet article avec un proche ou un collègue intéressé.

Si vous pensez que les explications vont un peu vite, n’hésitez pas à me le faire savoir  à l’adresse : bonjour@alixfdm.fr.

Je suis ouvert à toutes discussions.

Les sources du projet sont téléchargeable sur GitHub : https://github.com/afdm/simple-responsive-menu.