Créer un menu déroulant en CSS

Ce nouvel article de la série « CSS par la pratique » sera assez dense. Nous allons créer un menu déroulant tel que celui présent sur le site Gimme Some Oven.

Par rapport aux dispositions des articles précédents, celle-ci est plus complexe. Elle nous amènera à mettre en œuvre plusieurs concepts CSS :

  • Utilisation de la propriété mask-image pour afficher un SVG tout en permettant de modifier sa couleur via CSS
  • Utilisation des pseudo-éléments ::before et ::after pour afficher le triangle au-dessus des éléments du menu
  • Gestion de la visibilité du menu via la pseudo-classe :hover qu’on a déjà rencontré, couplée à l’utilisation de la propriété display

Cet article sera en fait en deux parties. La première ne différera pas des précédents articles de CSS par la pratique.

Par contre, dans la seconde, nous verrons pourquoi et comment utiliser SASS (Syntactically Awesome Style Sheets) plutôt que du CSS « pur » pour avoir des feuilles de style plus maintenables. Surtout, nous introduirons aussi l’automatiseur de tâches (task runner) Gulp, qui nous permettra :

  • de transformer automatiquement des feuilles de style SASS en CSS
  • de vérifier nos feuilles SASS ou CSS grâce aux outils d’analyse statique (linter)
  • d’optimiser les images SVG et les injecter directement dans notre code HTML

Commençons cependant par nous intéresser à la partie HTML et CSS de cet article. Alors, accrochez vos ceintures, c’est parti !

Création des liens avec une icône

Dans un premier temps, nous allons créer le CSS et le HTML nécessaire à la création des liens « Accueil », « Recette » et autres.

En ce qui concerne les icônes, nous avons déjà vu comment intégrer celles de Font Awesome. Si le procédé est simple, il souffre de plusieurs inconvénients. Parmi eux, la lourdeur des transferts et l’utilisation de Javascript.

Nous allons ici intégrer les icônes au format SVG dans le CSS de la page. Par la suite, nous les intégrerons directement dans le fichier HTML grâce à la balise svg, en utilisant Gulp.

De nombreux sites proposent des icônes SVG, dont certains ne nécessitent pas d’attribution. C’est le cas de UXWing, que nous utiliseront dans cet article.

Une recherche de « home » sur la page d’accueil de UXWing amène plusieurs résultats. J’ai choisi l’icône qui se rapproche visuellement le plus de celle de Gimme Some Oven, nommée home.svg. Le téléchargement au format SVG s’effectue via le bouton du milieu.

Pour les lecteurs qui n’ont pas l’habitude de manipuler des fichiers SVG, un point important à savoir est que ce sont des fichiers au format texte; on peut donc les ouvrir et les modifier avec n’importe quel éditeur. En particulier, on peut copier-coller leur contenu dans un document HTML.

Ici, plutôt que de mettre les icônes dans le HTML, nous allons les intégrer dans le CSS. Pour ce faire, il faut d’abord encoder le SVG de façon à ce qu’il puisse être utilisable en CSS. Cela consiste principalement à remplacer certains caractères tels que les points d’interrogation ou les slashs par des séquences du type %3F. On va utiliser dans ce but utiliser URL encoder for SVG.

Il suffit de coller le contenu du SVG dans la boite de saisie en haut à gauche; le résultat à utiliser sera disponible en bas à gauche, sous le libellé « Ready for CSS ».

La règle CSS fournie par le site utilise la propriété background-image. Cependant, celle-ci a l’inconvénient de ne pas permettre de modifier la couleur de l’image.

Nous allons du coup la remplacer par la propriété mask-image, qui s’utilise quasiment de la même façon. La couleur du masque peut alors être spécifiée via la propriété background-color.

Nous allons attribuer à ce bloc la classe square-menu-item; ce sera un lien comprenant l’icône au format SVG (avec la classe square-menu-item__icon) ainsi que le libellé du lien.

<!doctype html>

<html lang="fr">
<head>
  <meta charset="utf-8">
  <link rel="stylesheet" type="text/css" href="style.css" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>

<body>
  <a class="square-menu-item" href="/">
    <span class="square-menu-item__icon square-menu-item__icon--home"></span>
    Accueil
  </a>
</body>

Le CSS correspondant est assez simple. Nous utilisons Flexbox pour créer un conteneur flexible vertical avec des éléments alignés verticalement.

Le lien est passé en majuscule et sans soulignement, en ajoutant un peu d’espacement entre les lettres. Nous nous occuperons ultérieurement du problème de la fonte à changer.

:root {
  --couleur-fond: white;
  --couleur-principale: black;
}

html {
  box-sizing: border-box;

  background-color: var(--couleur-fond);
}

.square-menu-item {
  display: flex;
  flex-direction: column;

  align-items: center;
  padding: 1em;

  color: var(--couleur-principale);
  font-weight: bold;
  letter-spacing: 0.1em;
  text-decoration: none;
  text-transform: uppercase;
}

.square-menu-item__icon {
  width: 1em;
  height: 1em;

  margin-bottom: 0.25em;
  background-color: currentcolor;
  mask-repeat: no-repeat;
}

.square-menu-item__icon--home {
  mask-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3Csvg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 122.88 112.07' style='enable-background:new 0 0 122.88 112.07' xml:space='preserve'%3E%3Cstyle type='text/css'%3E.st0%7Bfill-rule:evenodd;clip-rule:evenodd;%7D%3C/style%3E%3Cg%3E%3Cpath class='st0' d='M61.44,0L0,60.18l14.99,7.87L61.04,19.7l46.85,48.36l14.99-7.87L61.44,0L61.44,0z M18.26,69.63L18.26,69.63 L61.5,26.38l43.11,43.25h0v0v42.43H73.12V82.09H49.49v29.97H18.26V69.63L18.26,69.63L18.26,69.63z'/%3E%3C/g%3E%3C/svg%3E");
}

Notez la valeur de propriété background-color de l’icône : on utilise la variable CSS currentColor. Ainsi, quand on changera la couleur de .square-menu-item, la couleur de l’icône sera impactée. Cette technique permet de garder les deux couleurs identiques en ne modifiant qu’une propriété.

Enfin, on assigne à la propriété mask-repeat la valeur no-repeat. Comme son nom l’indique, elle évite la répétition de l’icône.

Nous voici avec notre première icône :

L’ajout des trois autre icônes se fait de la même façon. Ajoutons donc dans la feuille CSS les icônes Recettes, Voyages et Style de vie. Nous encodons pour cela les icônes food-restaurant, heart-black et plane.

.square-menu-item__icon--food-restaurant {
  mask-image: url("data:image/svg+xml,%3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 122.88 115.21'%3E%3Cg%3E%3Cpath d='M29.03,100.46l20.79-25.21l9.51,12.13L41,110.69C33.98,119.61,20.99,110.21,29.03,100.46L29.03,100.46z M53.31,43.05 c1.98-6.46,1.07-11.98-6.37-20.18L28.76,1c-2.58-3.03-8.66,1.42-6.12,5.09L37.18,24c2.75,3.34-2.36,7.76-5.2,4.32L16.94,9.8 c-2.8-3.21-8.59,1.03-5.66,4.7c4.24,5.1,10.8,13.43,15.04,18.53c2.94,2.99-1.53,7.42-4.43,3.69L6.96,18.32 c-2.19-2.38-5.77-0.9-6.72,1.88c-1.02,2.97,1.49,5.14,3.2,7.34L20.1,49.06c5.17,5.99,10.95,9.54,17.67,7.53 c1.03-0.31,2.29-0.94,3.64-1.77l44.76,57.78c2.41,3.11,7.06,3.44,10.08,0.93l0.69-0.57c3.4-2.83,3.95-8,1.04-11.34L50.58,47.16 C51.96,45.62,52.97,44.16,53.31,43.05L53.31,43.05z M65.98,55.65l7.37-8.94C63.87,23.21,99-8.11,116.03,6.29 C136.72,23.8,105.97,66,84.36,55.57l-8.73,11.09L65.98,55.65L65.98,55.65z'/%3E%3C/g%3E%3C/svg%3E");
}

.square-menu-item__icon--heart {
  mask-image: url("data:image/svg+xml,%3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 122.88 107.41'%3E%3Cg%3E%3Cpath d='M60.83,17.19C68.84,8.84,74.45,1.62,86.79,0.21c23.17-2.66,44.48,21.06,32.78,44.41 c-3.33,6.65-10.11,14.56-17.61,22.32c-8.23,8.52-17.34,16.87-23.72,23.2l-17.4,17.26L46.46,93.56C29.16,76.9,0.95,55.93,0.02,29.95 C-0.63,11.75,13.73,0.09,30.25,0.3C45.01,0.5,51.22,7.84,60.83,17.19L60.83,17.19L60.83,17.19z'/%3E%3C/g%3E%3C/svg%3E");
}

.square-menu-item__icon--plane {
  mask-image: url("data:image/svg+xml,%3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 122.88 122.88'%3E%3Cg%3E%3Cpath d='M16.63,105.75c0.01-4.03,2.3-7.97,6.03-12.38L1.09,79.73c-1.36-0.59-1.33-1.42-0.54-2.4l4.57-3.9 c0.83-0.51,1.71-0.73,2.66-0.47l26.62,4.5l22.18-24.02L4.8,18.41c-1.31-0.77-1.42-1.64-0.07-2.65l7.47-5.96l67.5,18.97L99.64,7.45 c6.69-5.79,13.19-8.38,18.18-7.15c2.75,0.68,3.72,1.5,4.57,4.08c1.65,5.06-0.91,11.86-6.96,18.86L94.11,43.18l18.97,67.5 l-5.96,7.47c-1.01,1.34-1.88,1.23-2.65-0.07L69.43,66.31L45.41,88.48l4.5,26.62c0.26,0.94,0.05,1.82-0.47,2.66l-3.9,4.57 c-0.97,0.79-1.81,0.82-2.4-0.54l-13.64-21.57c-4.43,3.74-8.37,6.03-12.42,6.03C16.71,106.24,16.63,106.11,16.63,105.75 L16.63,105.75z'/%3E%3C/g%3E%3C/svg%3E%0A");
}

Hiérarchie d’un menu en HTML

Afin de garder des exemples lisibles, nous ne créerons ici que le sous-menu correspondant à « Recettes ».

De façon peu intuitive, les menus sont représentés par les éléments ul ou ol. Chaque élément du menu (ici, les icône avec liens) seront donc des li.

Pour créer les sous-menus, il suffit d’appliquer ce même principe récursivement, en ajoutant un élément ul dans le li.

Enfin, le menu est aussi inséré dans un élément nav indiquant un bloc de navigation.

Voici une version sans les classes, afin de voir plus facilement sa structure :

<nav>
  <ul>
    <li>
      <a href="/">
        Accueil
      </a>
    </li>
    <li>
      <a href="/recettes/">Recettes</a>
      <ul>
        <li>
          <a href="/recettes/">Parcourir les recettes</a>
        </li>
        <li>
          <a href="/index-recettes/">Index des recettes</a>
        </li>
        <li>
          <a href="/recettes-video/">Recettes en vidéo</a>
        </li>
      </ul>
    </li>
    <li>
      <a href="/voyages/">
        Voyages
      </a>
    </li>
    <li>
      <a href="/style-de-vie/">
        Style de vie
      </a>
    </li>
  </ul>
</nav>

Sa visualisation nous permet de vérifier que la structure est correcte.

Nous rajoutons quelques classes, toujours en utilisant la notation BEM :

  • top-menu pour le bloc principal du menu
  • top-menu__menu-list pour la liste d’élements du menu
  • top-menu__menu-item pour les éléments de cette liste
  • enfin, top-menu__sub-menu-list, top-menu__sub-menu-item pour le sous-menu et ses éléments et top-menu__sub-menu-link pour le lien qu’il contient.

On remarque qu’on peut affecter des classes BEM à des éléments situés à une profondeur arbitraire; on n’est pas limités aux éléments descendant directement du bloc.

Au final, le code du menu intégrant les images est assez dense – la faute principalement aux identifiants de classes BEM, très verbeux – :

<nav class="top-menu">
  <ul class="top-menu__menu-list">
    <li class="top-menu__menu-item">
      <a class="square-menu-item" href="/">
        <span class="square-menu-item__icon square-menu-item__icon--home"></span>
        Accueil
      </a>
    </li>
    <li class="top-menu__menu-item">
      <a class="square-menu-item" href="/recettes/">
        <span class="square-menu-item__icon square-menu-item__icon--food-restaurant"></span>
        Recettes
      </a>
      <ul class="top-menu__sub-menu-list">
        <li class="top-menu__sub-menu-item">
          <a class="top-menu__sub-menu-link" href="/recettes/">Parcourir les recettes</a>
        </li>
        <li class="top-menu__sub-menu-item">
          <a class="top-menu__sub-menu-link" href="/index-recettes/">Index des recettes</a>
        </li>
        <li class="top-menu__sub-menu-item">
          <a class="top-menu__sub-menu-link" href="/recettes-video/">Recettes en vidéo</a>
        </li>
      </ul>
    </li>
    <li class="top-menu__menu-item">
      <a class="square-menu-item" href="/voyages/">
        <span class="square-menu-item__icon square-menu-item__icon--plane"></span>
        Voyages
      </a>
    </li>
    <li class="top-menu__menu-item">
      <a class="square-menu-item" href="/style-de-vie/">
        <span class="square-menu-item__icon square-menu-item__icon--heart"></span>
        Style de vie
      </a>
    </li>
  </ul>
</nav>

Nous voici donc avec un menu moche mais complet !

Styler le menu

Nous allons dans un premier temps cacher le sous-menu pour pouvoir nous concentrer sur le menu principal.

.top-menu__sub-menu-list {
  display: none;
}

La tâche suivante est assez simple. Nous utilisons Flexbox pour faire de la liste du menu principal un conteneur horizontal, dont nous spécifions la taille. Nous allons aussi supprimer les marqueurs de la liste ainsi que ses marges internes.

.top-menu {
  width: 800px;
  height: 150px;
}

.top-menu__menu-list {
  display: flex;
  flex-direction: row;

  padding: 0;

  list-style-type: none;
}

Avec ces quelques lignes de CSS, le résultat est déjà bien meilleur !

Utiliser la position CSS pour aligner le sous-menu sous le menu

Occupons-nous maintenant du sous-menu. Dans un premier temps, on le rendra visible en permanence. On s’occupera ultérieurement de ne l’afficher que quand le curseur se trouve sur le menu.

Tout comme le menu principal, on va supprimer la marge interne et les marqueurs. Autre similarité, nous utiliserons aussi Flexbox, mais cette fois-ci pour créer un conteneur vertical.

Supprimons-donc le display: none; pour la classe top-menu__sub-menu-list et remplaçons cette propriété par les propriétés suivantes :

.top-menu__sub-menu-list {
  display: flex;
  flex-direction: column;

  padding: 0;

  background-color: black;
  color: white;
  font-size: 0.8em;
  font-weight: normal;
  list-style-type: none;
  text-align: center;
}

.top-menu__sub-menu-item {
  padding: 5px;
}

.top-menu__sub-menu-link {
  color: inherit;
  text-decoration: none;
}

Les choses prennent forme !

Il nous reste cependant à gérer le changement de couleur lors du passage du curseur, la visibilité du sous-menu ainsi qu’à afficher un triangle entre le menu principal et le sous-menu.

Utiliser le pseudo-éléments ::before

Pour afficher ce triangle, nous allons tout d’abord voir comment le dessiner. L’astuce est assez simple : il est possible de définir une bordure différente pour les quatre côtés haut / droite / bas / gauche via les propriétés border-top, border-right, etc.. Si on définit uniquement celle du bas en rendant les trois autres transparentes, on obtient un triangle !

Insérons donc un div entre le lien des recettes et la liste du sous-menu :

<a class="square-menu-item" href="/recettes/">
  <span class="square-menu-item__icon square-menu-item__icon--food-restaurant"></span>
    Recettes
</a>
<div class="top-menu__sub-menu-list-triangle"></div>

On va créer un triangle grâce au CSS suivant :

.top-menu__sub-menu-list-triangle {
  width: 0;
  height: 0;

  border-right: 10px solid transparent;
  border-bottom: 10px solid black;
  border-left: 10px solid transparent;
}

Il nous reste encore à le centrer horizontalement, mais c’est un bon début.

Cependant, c’est un peu fastidieux de créer un div uniquement pour cet effet. À la place, nous allons utiliser le pseudo-élément ::before, plus précisément .top-menu__sub-menu-list::before.

Comme son nom l’indique, il est placé avant l’élément .top-menu__sub-menu-list. Par défaut il n’est pas visible; il faut lui affecter du contenu via la propriété content. Oublier de spécifier cette propriété est une cause fréquente de frustration quand on travaille avec ::before !

Supprimons donc notre div avec la classe .top-menu__sub-menu-list-triangle, et remplaçons le CSS de cette même classe par :

.top-menu__sub-menu-list::before {
  width: 0;
  height: 0;

  border-right: 10px solid transparent;
  border-bottom: 10px solid black;
  border-left: 10px solid transparent;

  content: "";
}

Et là, c’est le drame ! En rechargeant la page, le triangle n’est plus du tout visible. Si pour tester on passe la couleur de la bordure du bas à red, on s’aperçoit qu’il est bien là, mais qu’il est invisible.

En fait, par défaut, l’élément ::before est traité comme le premier fils de l’élément auquel il se rapporte. Ainsi, on affiche un triangle noir sur la liste, qui est affichée avec un fond noir.

Il va donc falloir faire en sorte que le triangle soit positionné avant la liste. Pour cela, nous allons une fois de plus recourir à la propriété position.

Ajoutons tout d’abord au parent (top-menu__sub-menu-list) la règle position: relative.

Ensuite, on va positionner le triangle de façon absolue par rapport à son parent. Pour le centrer horizontalement, on lui affecte la propriété left: 50% (le pourcentage se rapportant à la largeur totale du parent). Ensuite, de façon assez peu intuitive, on lui affecte la propriété bottom: 100%.

Pourquoi pas top: 0 plutôt ? Cette version conduirait à avoir la haut du triangle (plutôt que le bas) en haut du parent. Il serait donc une fois de plus invisible. Par contre, mettre le bas du triangle à 100% de la hauteur du parent (mesurée depuis en bas) amène bien à l’avoir à la position désirée.

Ces propriétés nécessitent un peu de gymnastique intellectuelle lors des premières utilisations. N’hésitez donc pas à jouer avec pour mieux comprendre leur fonctionnement !

Voici la version à laquelle on arrive :

.top-menu__sub-menu-list::before {
  position: absolute;
  bottom: 100%;
  left: 50%;

  width: 0;
  height: 0;

  border-right: 10px solid transparent;
  border-bottom: 10px solid black;
  border-left: 10px solid transparent;

  content: "";
}

Le résultat est presque parfait, si ce n’est l’habituel problème de centrage du triangle. En effet, il débute à la moitié de la largeur de son parent, au lieu d’être totalement centré.

Heureusement, la solution est triviale. Il suffit d’effectuer une translation de la moitié de sa largeur vers la gauche pour qu’il retrouve un centrage parfait. Cela se traduit par la règle transform: translateX(-50%). On y est enfin !

Deux remarques supplémentaires sur le pseudo-élément ::before. Tout d’abord, comme on peut s’en douter, il existe l’équivalent ::after. Celui-ci fonctionne de façon équivalente.

Ensuite, vous rencontrerez parfois la notation :before et :after. Cette notation a été modifiée en ::before et ::after afin de différencier ces pseudo-éléments des pseudo-classes telles que :hover, dont le but est fondamentalement différent. Les deux notations sont comprises par les navigateurs, mais mieux vaut utiliser la notation avec le double deux-points car elle correspond aux standards actuels.

Gestion du passage du curseur avec :hover

Cette partie sera beaucoup plus facile que la précédente. On va changer la couleur du texte et de l’icône du menu principal quand le curseur passe dessus. De même, on modifiera la couleur de fond du sous-menu pour indiquer l’élément courant.

.square-menu-item:hover {
  color: #f4c440;
}

.top-menu__sub-menu-item:hover {
  background-color: #f4c440;
}

Sur le site Gimme Some Oven, on peut constater que le passage d’une couleur à l’autre n’est pas instantané. Pour réaliser cet effet, il faut utiliser les transitions CSS qui indiquent comment passer d’une valeur de propriété à une autre de façon graduelle.

Ainsi, pour le sous-menu, on indique la durée de la transition, les propriétés auxquelles appliquer la transition, et la fonction modélisant la transition.

.top-menu__sub-menu-item {
  transition-duration: 0.2s;
  transition-property: background-color;
  transition-timing-function: ease-in-out;
}

De même, pour les éléments du menu principal :

.square-menu-item {
  transition-duration: 0.2s;
  transition-property: color;
  transition-timing-function: ease-in-out;
}

Notre menu prend vraiment forme. Il nous reste à cacher le sous-menu quand le curseur n’est pas sur le menu correspondant. Pour cela, on lui affecte la règle display:none quand le curseur n’est pas sur son parent. C’est l’occasion d’utiliser :not, qui permet d’inverser une condition.

.top-menu__menu-item:not(:hover) .top-menu__sub-menu-list {
  display: none;
}

Ultime souci : quand on déroule maintenant le sous-menu, les marges droite et gauche autour de l’icône s’agrandissent afin que celles-ci compensent la largeur du sous-menu.

Pour éviter ce phénomène, nous allons rendre le lien et l’icône du menu principal indépendants du sous-menu. Concrètement, nous allons à nouveau utiliser la propriété position pour positionner le sous-menu de façon absolue.

La première étape est de déclarer le parent top-menu__menu-item comme ayant un positionnement relatif. Ensuite, le raisonnement est exactement le même que lors du placement du triangle : on place le sous-menu tout en bas de son parent via top: 100%, on le centre en lui affectant left: 50% et une translation vers la gauche de la moitié de sa largeur. Ici, on lui affecte aussi une largeur fixe. Cela lui donnera une apparence homogène si d’autres sous-éléments sont définis.

.top-menu__menu-item {
  position: relative;
}

.top-menu__sub-menu-list {
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);

  width: 200px;
}

Intégration de la fonte Brandon Grotesque

J’ai téléchargé la fonte Brandon Grotesque sur BestFonts.pro. C’est un site assez désagréable à utiliser, avec des publicités très intrusives; cependant, il a le bon goût de fournir une feuille de style correspondant aux différentes versions des polices.

On va donc copier-coller les règles qui nous intéressent dans notre propre fichier CSS, après avoir extrait les fontes dans un sous-répertoire fonts (le chemin des src a été ici modifié pour prendre en compte ce sous-répertoire).

On utilise la variante en gras, plus proche visuellement de celle de Gimme Some Oven.

Dernière remarque, n’oubliez pas de placer ces règles au début du fichier !

@font-face {
  font-family: "Brandon Grotesque";
  font-style: normal;
  font-weight: bold;

  src: url("fonts/BrandonGrotesque-Bold.eot");
  src: local("Brandon Grotesque Bold"), local("BrandonGrotesque-Bold"),
    url("fonts/BrandonGrotesque-Bold.eot?#iefix") format("embedded-opentype"),
    url("fonts/BrandonGrotesque-Bold.woff2") format("woff2"),
    url("fonts/BrandonGrotesque-Bold.woff") format("woff"),
    url("fonts/BrandonGrotesque-Bold.ttf") format("truetype");
}

Il faut bien sûr aussi ajouter les règles font-family: "Brandon Grotesque" et font-weight: bold à la classe top-menu.

Cette fois-ci, c’est terminé !

Dans le prochaine article de la série, nous verrons comment utiliser Gulp pour faciliter l’utilisation des icônes SVG et avoir du CSS plus maintenable.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *