Un menu CSS horizontal multi-niveaux et responsive
19 décembre 2016.
Introduction
Bon, je vous l’accorde volontiers, ce sujet a été développé, analysé, décortiqué, traité et retraité jusqu’à plus soif…
Qui suis-je donc, moi, misérable petite larve laborieusement codeuse, pour prétendre avoir quelque chose de neuf à apporter à cette problématique par ailleurs pas plus passionnante que ça ?
Qui suis-je ?
Euh… Bonne question que je me renvoie régulièrement en pleine poire mais à laquelle je ne suis pas pour autant persuadé d’apporter un jour ne serait-ce qu’un semblant de réponse. À moins de me résumer abruptement à un vieux con qui aime se la péter en ramenant sa fraise aussitôt qu’il croit naïvement avoir quelque chose à dire…
Mais bon, je m’égare : on n’était pas parti pour philosopher façon comptoir mais bien plutôt pour causer menu…
Car ce sujet maintes fois rebattu demeure l’occasion toujours renouvelée d’explorer les arcanes du langage CSS et de ses évolutions. En l’occurrence, nous tenterons ici de mettre en place une solution élégante de manière à animer en fade-in et out les apparitions et disparitions des sous-menus au survol de leurs titres. Sur notre lancée, nous exploiterons sans vergogne les pseudos-éléments pour donner un peu de sel aux événements hover survenant sur les liens. Et nous ne manquerons évidemment pas d’aborder la réalisation du fameux hamburger si cher aux écrans de nos smartphones. Tout ceci en nous efforçant autant qu’il est possible de ne pas céder à la tentation javascript.
Comme je vous sais impatients de voir, je vous invite cordialement et sans plus tarder à satisfaire votre curiosité en vous rinçant l’œil ici.
Au commencement était <ul>
Pour les plus novices d’entre vous, il n’est peut-être pas inutile de préciser que la structure traditionnelle d’un menu se construit à partir d’une liste non ordonnée dont chacune des lignes renvoie à un élément dudit menu en intégrant un lien vers la page idoine.
En clair, on travaille sur une base du genre que voici :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<!DOCTYPE html> <html> <head> <title>Démonstration Menu Horizontal Responsive</title> <meta charset="utf-8"> </head> <body> <div class="menuContainer"> <ul> <li><a href="#accueil">Accueil</a></li> <li><a href="#entrees">Entrées</a></li> <li><a href="#plats">Plats</a></li> <li><a href="#desserts">Desserts</a></li> <li><a href="#photos">Photos</a></li> </ul> </div> </body> </html> |
Et si l’on souhaite élargir le choix en intégrant des sous-menus, on décline bêtement la structure maîtresse :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
<!DOCTYPE html> <html> <head> <title>Démonstration Menu Horizontal Responsive</title> <meta charset="utf-8">> </head> <body> <div class="menuContainer"> <ul> <li><a href="#accueil">Accueil</a></li> <li>Entrées <ul> <li><a href="#oeuf">Oeuf mayonnaise</a></li> <li><a href="#celeri">Céleri remoulade</a></li> <li><a href="#hareng">Filets de hareng pommes à l'huile</a></li> <li><a href="#carottes">Carottes rapées</a></li> </ul> </li> <li>Plats <ul> <li><a href="#foie">Foie de veau sauce échalote</a></li> <li><a href="#escalope">Escalope de dinde sauce Normande</a></li> <li><a href="#jarret">Jarret de porc grillé</a></li> </ul> </li> <li>Desserts <ul> <li><a href="#fromages">Assiette de fromages</a></li> <li><a href="#mousse">Mousse au chocolat</a></li> <li><a href="#creme">Crème brulée</a></li> </ul> </li> <li><a href="#photos">Photos</a></li> </ul> </div> </body> </html> |
Si on s’arrête là, on se retrouve face au visuel que voici :
See the Pen MenuHorizontal001 by Amstramgram (@Amstramgram) on CodePen.24505
Alors bon… Oui… Je vous le concède : c’est pas joli-joli et je sens bien que vous êtes un peu déçus, voire même carrément désappointés, si ce n’est pas tout au bord de refermer rageusement cette page pour vous en aller voir ailleurs si la vie n’y serait pas plus belle et je vous confirme qu’elle l’est bel et bien…
Sauf que… Ce à quoi vos yeux effarés sont ici confrontés n’est rien de plus ou de moins que la mise en forme promptement effectuée par votre navigateur favori lequel, s’il ne fait pas dans la fioriture, assure néanmoins le minimum syndical en fonction de ce qu’on lui a demandé d’afficher, à savoir des listes de liens plus ou moins imbriquées les unes dans les autres.
Or donc, première chose à faire : nous débarrasser sans plus tarder du daté design que nous propose par défaut notre navigateur en opérant un reset CSS. Pour l’occasion qui nous occupe, il se réduira au plus strict nécessaire mais vous n’ignorez sans doute pas qu’en production normale, il peut s’avérer légèrement plus conséquent.
Toutefois, comme vous pouvez le constater ci-dessous non sans un certain effroi désespéré, cette remise à plat ne suffit pas à faire notre bonheur et il semblerait bien comme ça à première vue que nous soyons encore fort éloignés du résultat que nous espérons atteindre :
See the Pen MenuHorizontal002 by Amstramgram (@Amstramgram) on CodePen.24505
Pourtant, quelques simples petits aménagements vont suffire à nous en rapprocher. La preuve :
See the Pen MenuHorizontal003 by Amstramgram (@Amstramgram) on CodePen.24505
Rien ici de franchement miraculeux : je me suis contenté de grossir un peu la police, fixer la largeur du container principal à 70% de la fenêtre tout en la limitant à 600px au max et assurer le centrage de l’ensemble via un basique margin:auto.
Par ailleurs, j’ai passé le display de la liste maîtresse en flex pour pouvoir exploiter sans retenue sa propriété justify-content en space-between. Les différents éléments du menu principal sont ainsi répartis de manière homogène, séparés les uns des autres par un espace de largeur identique.
Enfin, les sous-menus sont (provisoirement) retirés de l’affichage via un abrupt display:none, genre on élimine le problème avant que de s’y attaquer.
Les sous-menus
Pour afficher les sous-menus, je pourrais peu ou prou me contenter de passer leur propriété display de none à block. Cependant, ce serait aller à l’encontre de ma compulsive propension à couper les cheveux en 4 et de mon penchant pour les transitions douces. Je tiens à un effet de fadeIn/fadeOut et n’en démords pas. Ah mais !
Qu’à cela ne tienne, me direz vous : il suffit d’opérer une transition sur l’opacité et le tour est joué !
Sauf que non ! Cela ne suffit pas.
Pour en faire la démonstration, considérons le cas où l’opacité des sous-menus est réduite à zéro. Ces derniers se trouvent alors effectivement dissimulés aux regards mais ils n’en demeurent pas moins présents dans le flux et continuent donc imperturbablement de réagir aux entrées/sorties de la souris.
Pour palier ce problème, il nous faut impérativement forcer leur visibilité à hidden.
Mais si la visibilité est passée à hidden, adieu la transition sur l’opacité !
À moins que…
À moins que l’on ne trouve un subterfuge pour ne modifier la visibilité qu’une fois que la transition sur l’opacité a atteint son terme.
Et justement, les transitions pensées par CSS ont l’extrême amabilité de disposer d’une propriété delay à laquelle il serait particulièrement nouille de ne pas recourir dans le cas qui nous occupe.
Ainsi :
1 2 3 4 5 |
visibility: hidden; opacity: 0; transition: opacity .2s ease-in-out, visibility 0s ease-in-out .2s; |
définit une première transition destinée à ramener l’opacité de l’élément traité à 0 en 200ms et une seconde portant sur sa visibilité, s’effectuant en un temps nul mais après un délai judicieusement établi à 200ms.
Dernier petit souci dans la configuration actuelle des choses : les sous-menus rétablis dans le flux viennent rompre l’élégante répartition uniforme que nous avions établie via le justify-content: space-between; posé sur le menu principal. Pour corriger ce point, la solution la plus simple consiste à les positionner en absolute non sans avoir omis au préalable d’inscrire un position: relative; sur leur élément parent.
Nous en parvenons alors à l’état suivant :
See the Pen MenuHorizontal004 by Amstramgram (@Amstramgram) on CodePen.24505
Les plus attentifs d’entre vous n’auront pas manqué de remarquer les durées différentes définies pour l’anecdote sur nos deux transitions : 500ms sur l’apparition et 200ms sur la disparition. Ils auront également noté la propriété cursor: default; posée sur les éléments lignes qui permet de nous affranchir du moche curseur text, lequel chatouillait mes yeux sensibles sur les titres des sous-menus.
Animations des éléments
En des temps immémoriaux, alors que je défoulais mes pulsions codeuses à grands coups de Flash (j’vous avais prévenu ! ça ne nous rajeunit pas !), j’avais travaillé sur un effet permettant de souligner le passage de la souris sur des éléments de texte. J’ai usé et abusé de la chose sur bon nombre des sites construits de mes petites mains malhabiles, sites dont vous vous doutez bien qu’il ne reste plus guère de traces à ce jour (quoique, en cherchant bien…).
Cependant, ce qui restait particulièrement délicat à mettre en place via HTML et consorts il y a encore quelques années est devenu presque un jeu d’enfant à réaliser avec CSS3 et je me suis fort logiquement dit que ce serait bête de s’en priver…
Or donc, prenons un élément de texte désigné par normal et contenant par exemple le mot Accueil. On superpose à cet élément normal un élément dit over au contenu textuel identique à celui de normal et dont on fixe la largeur à 0 et l’overflow en hidden. Cet élément de largeur nulle et au débordement interdit est donc invisible par défaut. L’effet consiste à amener progressivement sa largeur à 100% de manière à ce qu’il recouvre l’élément normal en donnant l’illusion que celui-ci prend du gras…
Comme je suis un grand pervers, je me suis dit comme ça que ce serait plus classe encore si ce mouvement était accompagné par un discret effet d’ombrage. Pour ce faire, on ajoute un troisième élément que nous affublerons du petit nom de shadow, positionné en absolute à gauche de notre élément normal et que nous emmènerons à la droite de celui-ci au même rythme que nous révélons l’élément over… Pour peu qu’on ait pris soin de poser un overflow: hidden; sur normal, notre élément shadow n’est visible que lors de ses déplacements.
Pour tirer profit au mieux des avancées de CSS3 et surtout frimer un max, over et shadow seront construits en tant que pseudo-éléments ::before et ::after de notre pièce maîtresse normal.
Comme toujours, une démonstration en vraie de chez sœur Véridique vaut bien mieux que de longs et embrouillés discours :
See the Pen TextAnim by Amstramgram (@Amstramgram) on CodePen.24505
J’ai pris ici un malin plaisir à exagérer l’effet avec un rouge bien pétant et un temps de transition particulièrement lent pour en marquer la mécanique.
Le menu, quant à lui, peut être traité avec un peu plus de finesse.
En pratique, l’animation sera portée par les ancres dont nous devons avant tout veiller à passer la propriété display sur inline-block.
Par ailleurs, il est sans doute bon de rappeler que le contenu textuel des pseudo-éléments est défini par leur propriété content, laquelle peut faire référence à un attribut porté par l’élément d’origine. Autrement dit, si nous avons :
1 2 3 |
<a href="#accueil" name="Accueil">Accueil</a> |
et :
1 2 3 4 5 |
a::before{ content:attr(name); } |
Le pseudo-élément ::before associé contiendra le texte Accueil.
Il nous faut donc songer sans tarder à amender notre structure HTML en conséquence et voici ce que nous obtenons :
See the Pen MenuHorizontal005 by Amstramgram (@Amstramgram) on CodePen.24505
Au rayon des détails, j’ai très légèrement augmenté la valeur de line-height sur le container principal de manière à éviter la troncature des jambages des lettres provoquée par les différents overflow:hidden; et j’ai ménagé une infime marge supérieure sur les sous-menus pour les dégager verticalement de leurs titres.
Mise en forme responsive
Nous avons réalisé une bonne moitié de notre mission. Il nous faut maintenant envisager un design plus adapté à des écrans de faible largeur. Nous allons mettre à leur disposition le traditionnel hamburger qui dévoilera d’un clic le menu et ses sous-menus. Un second clic permettra de refermer élégamment la bête.
La problématique est souvent évoquée sur la toile : si le langage CSS autorise la gestion de l’événement hover, il ne permet pas de réagir à l’événement click. Tout au moins si on ne le lui demande pas gentiment. Il existe en effet un certain nombre de techniques qui permettent de combler ce manque. En l’occurrence, nous allons recourir à la plus courante, celle dite du checkbox hack, qui consiste à se servir d’un input de type checkbox dont CSS va nous permettre d’exploiter l’état checked. Cet input est soigneusement dissimulé via un display:none; et c’est en fait un label judicieusement stylé qui fait office d’interface.
Illustration par l’exemple :
See the Pen CheckboxHack by Amstramgram (@Amstramgram) on CodePen.24505
Outre le display:none; posé sur l’élément input qui n’appelle pas plus de commentaire, vous noterez le display:flex; attribué au label qui permet d’assurer le centrage horizontal et vertical de ses enfants ::before et ::after via un simple et trivial margin:auto;. Le pseudo-élément ::before assure la signalétique hamburger tandis que son frangin ::after est occupé par une large croix invitant à refermer.
Ne nous reste alors plus qu’à mettre en place le dispositif d’apparition/disparition du menu réorganisé en nous appuyant très traditionnellement sur les medias queries. Nous considérerons que le menu mobile sera effectif dès lors que la largeur de l’écran descendra en dessous de la barre des 540 pixels (seuil en deçà duquel l’espace entre les entrées du menu principal devient franchement riquiqui) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
#menuHamburger{ display: none; } .menuContainer>label { display:none; cursor:pointer; vertical-align:top; background:gray; width:39px; height:39px; border-radius:5px; } .menuContainer>label::before { content:""; margin:6px auto; width:25px; height:5px; background:#000; box-shadow: 0 11px 0 0 #000, 0 22px 0 0 #000; } .menuContainer>label::after { display:none; content:"\2716"; font-weight:900; margin:auto; font-size:28px; } #menuHamburger:checked + ul{ margin-left:0px; } #menuHamburger:checked ~ label::after{ display:block; } #menuHamburger:checked ~ label::before{ display:none; } @media screen and (max-width:540px) { /*On annule le centrage du container*/ .menuContainer{ width:auto; margin:0; } /*Menu Principal*/ .menuContainer>ul { /*Pour assurer l'alignement avec le label*/ display:inline-block; /*On intègre le padding dans le calcul des dimensions de l'élément*/ box-sizing:border-box; /*On fixe sa largeur*/ width:260px; /*On le positionne en passant son margin-left à moins sa largeur*/ margin:0 0 0 -260px; /*On agrémente l'apparence*/ padding:10px; background:#ddd; border-radius:0 0px 10px 0; /*On pose une animation sur son margin-left lequel est passé à zéro lorsqu'on clique sur le label*/ transition:margin-left 0.5s ease-in-out; } /*Sous-menus*/ .menuContainer>ul>li>ul{ /*On annule le positionnement en absolute*/ position:static; /*On asure son affichage*/ visibility:visible; opacity:1; /*On décale pour faire joli*/ margin-left:10px; /*On bloque toutes les transitions*/ transition:none !important; } .menuContainer>label { display:inline-flex; } } |
Et nous obtenons alors le résultat que voici :
See the Pen MenuHorizontal006 by Amstramgram (@Amstramgram) on CodePen.24505
À noter que l’élément input est placé en tête de manière à pouvoir user sans souci des sélecteurs + et ~.
Par ailleurs, le label est collé directement sans espace ni retour ligne à la balise de clôture de liste </ul> pour s’affranchir des espaces indésirables inhérents aux éléments traités en inline-block.
Nous voici donc parvenus au bout de nos peines ou presque…
Les plus pointilleux d’entre vous auront en effet certainement remarqué quelques défauts résiduels. Pour ma part, j’en relève deux :
- lorsque l’on passe brutalement d’un affichage mobile à un affichage desktop, les sous menus apparaissent brièvement.
- dans le cas inverse, en passant d’un affichage desktop à mobile, le menu est ouvert et le demeure pour peu qu’il ait été ouvert de par le passé ou bien alors se referme sans autre forme de procès.
Pour l’essentiel, ces failles sont la conséquence directe et logique des transitions mises en place. Le seul moyen que mon esprit étriqué parvient à imaginer pour les corriger est de recourir à javascript.
L’idée est d’attribuer lors des redimensionnements une classe spécifique au body de notre page, classe qui bloque toutes les transitions par un radical transition:none !important; non sans veiller à retirer cette classe aussitôt que nécessaire. Pour ce faire, il serait carrément super génial pratique de disposer d’événements spécifiques signalant le début et la fin d’un redimensionnement. Vous pensez bien que nous ne sommes pas les premiers à exprimer ce besoin pressant et que forcément quelques solutions à notre problématique traînent un peu partout. Pour ma part, j’ai retenu celle-ci.
Ne nous reste donc plus qu’à mettre tout ceci en ordre et en mots :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
$(function() { var $menuHamburger = $('#menuHamburger'), $window = $(window), $body = $('body'), widthBefore = $window.width(); $menuHamburger.prop('checked',false); $window .on('resizestart', function(){ $body.addClass('noTransition'); }) .on('resizeend', function(){ if (widthBefore <= 540 && $window.width() > 540) { $menuHamburger.prop('checked', false); } widthBefore = $window.width(); $body.removeClass('noTransition'); }); $body.removeClass('noTransition'); }); //http://stackoverflow.com/a/29725076 (function ($) { var d = 250, t = null, e = null, h, r = false, $w = $(window); h = function () { r = false; $w.trigger('resizeend', e); }; $w.on('resize', function(ev){ e = ev || e; clearTimeout(t); if (!r) { $w.trigger('resizestart', e); r = true; } t = setTimeout(h, d); }); }(jQuery)); |
Je crois bien que nous sommes finalement venus à bout de la besogne à nous assignée.
Ceux d’entre vous qui le souhaitent peuvent sans complexe télécharger les sources de l’ensemble ici.
Pour ma part, je ne trouve plus grand chose à rajouter et, dans ce genre de cas, le silence est toujours une jolie option dont je me saisis sans plus tarder…