Un menu responsive en HTML/CSS sans Javascript
Une disposition courante pour un menu consiste à afficher les éléments à l’horizontal sur les écrans larges (de type PC) et d’avoir une liste déroulante sur les petits écrans (de type smartphone) en utilisant une icône « hamburger » pour l’ouvrir. De nombreux sites utilisent Javascript pour y parvenir ; il est pourtant possible de faire la même chose avec CSS uniquement.
Les différentes étapes de création du menu
- Pour commencer, nous créons une liste de liens contenus dans un élément
nav
avec une autre liste de liens pour le menu contenant un sous menu. - Dans cette liste, nous utilisons un
input
de typecheckbox
pour afficher ou non le menu sur les petits écrans. - Nous associons un label à cette
checkbox
et nous modifions son apparence qu’il prenne la forme d’un « hamburger » pour le menu principal et la forme d’un signe « + » pour les sous-menus. - Nous ajoutons deux animations :
- L’icône « hamburger » est animée pour qu’elle se transforme en une croix quand le menu est ouvert.
- L’icône « + » est animée pour qu’elle se transforme en signe moins quand le sous-menu est ouvert.
- Enfin, nous utilisons les media query pour que le menu devienne horizontal à partir d’une certaine largeur d’écran. Nous masquerons en même temps nos icônes et nos
checkbox
. - Dans ces media query, nous indiquons également que les sous-menus doivent s’afficher au survol de la souris ou au focus pour la navigation au clavier.
Notre menu responsive en détails
Un peu de HTML
Que ce soit pour notre menu principal ou pour les sous-menu, nous allons intégrer dans notre HTML les deux éléments qui suivent :
<input type="checkbox" id="submenu-toggle" name="submenu-toggle" aria-labelledby="submenu-toggle-label" class="submenu-toggle" />
<label for="submenu-toggle" class="top-nav-label" id="submenu-toggle-label">
<span class="open-close-menu">
<span class="open-menu">Ouvrir le sous-menu</span>
<span class="close-menu">Fermer le sous-menu</span>
</span>
</label>
La checkbox, une fois cochée, affichera notre menu. Le label prendra soit la forme d’un « hamburger » soit la forme d’un signe « plus ». Dans ce label, nous déclarons deux éléments span
pour les lecteurs d’écran. Quand le menu est fermé, le premier s’affichera ; quand le menu est ouvert, le second s’affichera.
Beaucoup de CSS
L’icône hamburger
/* Hamburger button */
.site-header > .top-nav-label {
position: relative;
right: 0;
top: 0;
height: 2.5rem;
width: 2.5rem;
border-radius: 0.1875rem;
cursor: pointer;
display: block;
font-size: 0;
margin: 0;
overflow: hidden;
padding: 0;
}
.site-header > .top-nav-label .open-close-menu {
display: block;
position: absolute;
z-index: 99999;
background: #1450aa;
border-radius: 0.1875rem;
height: 0.25rem;
left: 0.1875rem;
right: 0.1875rem;
top: 1.125rem;
}
.site-header > .top-nav-label .open-close-menu::before,
.site-header > .top-nav-label .open-close-menu::after {
border-radius: 0.1875rem;
content: "";
display: block;
position: absolute;
background-color: #1450aa;
height: 0.25rem;
left: 0;
width: 100%;
}
.site-header > .top-nav-label .open-close-menu::before {
top: -0.6875rem;
}
.site-header > .top-nav-label .open-close-menu::after {
bottom: -0.6875rem;
}
Dans cet extrait de code, la classe .open-close-submenu
et les pseudo-éléments before
et after
vont nous servir à créer les 3 lignes horizontales du « hamburger ». Les autres règles permettent de positionner notre icône et de lui définir une taille.
L’animation de l’icône au clic
.site-header > .top-nav-label .open-close-menu {
transition: 0.25s ease-in-out 0s;
}
.site-header > .top-nav-label .open-close-menu::before,
.site-header > .top-nav-label .open-close-menu::after {
transition-duration: 0.3s, 0.3s;
}
.site-header > .top-nav-label .open-close-menu::before {
transform-origin: top;
}
.site-header > .top-nav-label .open-close-menu::after {
transform-origin: bottom;
}
.site-header > [type="checkbox"]:checked ~ .top-nav-label .open-close-menu {
background: none;
transition-delay: 0s, 0.3s;
}
.site-header
> [type="checkbox"]:checked
~ .top-nav-label
.open-close-menu::before,
.site-header
> [type="checkbox"]:checked
~ .top-nav-label
.open-close-menu::after {
left: 0.125rem;
transition-delay: 0s, 0.3s;
}
.site-header
> [type="checkbox"]:checked
~ .top-nav-label
.open-close-menu::before {
top: 0;
transform: rotate(45deg);
}
.site-header
> [type="checkbox"]:checked
~ .top-nav-label
.open-close-menu::after {
bottom: 0;
transform: rotate(-45deg);
}
Nous rajoutons quelques lignes dans les précédentes règles pour indiquer la transformaton et le délai de transformation. Ensuite, nous indiquons en quoi doit se transformer l’icône au clic (quand la checkbox
est cochée donc). Ici, nous transformons l’icône « hamburger » en une croix avec la rotation de deux lignes et en masquant la troisième.
Les media queries
@media only screen and (min-width: 64em) {
.site-branding {
flex: 1 0 30%;
max-width: 30%;
}
.site-header input[type="checkbox"],
.site-header .top-nav-label {
display: none;
}
.main-navigation {
display: block;
flex: 1 0 70%;
max-width: 70%;
}
.main-navigation .menu {
border: none;
display: flex;
justify-content: flex-end;
}
.main-navigation .menu > .menu-item {
margin-left: 0.3125rem;
}
}
Ici, j’utilise flexbox
pour indiquer qu’à partir de 1024px (un iPad en mode paysage en gros) les éléments du menu doivent être à l’horizontal.
@media only screen and (min-width: 64em) {
.main-navigation .menu .menu-item-has-children > a::after {
display: inline-block;
content: "\25BC";
margin-left: 0.4045rem;
text-decoration: none;
position: relative;
}
.main-navigation .sub-menu {
min-width: 100%;
position: absolute;
border: 0.0625rem solid #dcdcdc;
}
.main-navigation .sub-menu ul {
left: -100%;
top: 0;
max-width: 100%;
}
}
Ensuite, nous modifions l’icône affichée à côté des sous-menu. Nous remplaçons le signe « + » par une flèche orientée vers le bas, comme il est courant de le voir. Enfin, nous repositionnons nos sous-menus.
Edge / Internet Explorer : l’éternel problème
Microsoft et ses navigateurs ont toujours posés divers problèmes en développement web. Avec Edge, la nouvelle version, – même s’il y a du mieux – il existe encore des comportements différents des autres navigateurs.
Selon CanIUse, Microsoft Edge supporte focus-within
; Internet Explorer ne le supporte pas. Pourtant, dans notre cas, cela ne fonctionne pas sur Edge non plus. Ainsi, la navigation au clavier pour les sous-menus ne fonctionne pas. De plus, il faudra spécifier les règles utilisant focus-within
séparément sinon aucune règle ne fonctionnera sur IE / Edge.
Pour que notre sous-menu apparaisse, il faudra donc écrire :
.main-navigation .menu-item-has-children > a:hover ~ ul,
.main-navigation .menu-item-has-children:hover > a ~ ul,
.main-navigation .menu-item-has-children > a ~ ul:hover,
.main-navigation .menu-item-has-children > a:focus ~ ul {
display: block;
z-index: 999;
pointer-events: auto;
}
.main-navigation .menu-item-has-children:focus-within > a ~ ul {
display: block;
z-index: 999;
pointer-events: auto;
}
Nous somme obligé de déclarer la ligne utilisant focus-within
séparemment pour qu’IE/Edge n’ignore pas les précédents styles.
Récupérer les fichiers
Je n’ai pas détaillé toutes les étapes dans les extraits publiés ci-dessus. Si vous récupérez les bouts de code, votre menu ne fonctionnera. Je vous ai simplement expliqué comment j’ai procédé.
Si vous souhaitez récupérer le contenu du fichier CSS, vous pouvez le faire ci-dessous. Le code pour le menu démarre à partir de .site-header
.
*,
*::after,
*::before {
box-sizing: inherit;
}
html {
box-sizing: border-box;
line-height: 1.15;
-webkit-text-size-adjust: 100%;
padding: 0;
margin: 0;
}
body {
font-family: Arial, "Liberation Sans", Helvetica, sans-serif;
font-size: 16px;
font-size: 1rem;
line-height: 1.618;
margin: 0;
}
.site {
font-size: 1.05rem;
position: relative;
word-wrap: break-word;
position: relative;
margin: 0 auto;
padding: 0 1.618rem;
}
.site-header {
display: flex;
justify-content: space-between;
flex-flow: row wrap;
align-items: center;
position: relative;
margin: 0 auto 2.427rem;
}
.site-branding {
flex: 1 0 80%;
max-width: 80%;
}
.site-title {
margin: 1.618rem 0 0;
line-height: 1;
font-size: 1.3rem;
font-weight: 400;
word-break: break-all;
}
.site-header > .top-nav-label {
flex: 0 0 auto;
}
.main-navigation {
display: none;
flex: 1 0 100%;
}
.main-navigation ul {
list-style-type: none;
margin: 0;
padding: 0;
text-align: left;
}
.main-navigation .menu {
position: relative;
border: 0.0625rem solid #dcdcdc;
width: 100%;
}
.main-navigation .menu::before {
border-bottom: 0.5625rem solid #dcdcdc;
border-left: 0.5625rem solid transparent;
border-right: 0.5625rem solid transparent;
border-top: 0 solid #dcdcdc;
content: "";
right: 0.625rem;
position: absolute;
top: -0.5625rem;
}
.main-navigation .menu::after {
border-color: #efefef transparent;
border-style: solid;
border-width: 0 0.5rem 0.5rem;
content: "";
right: 0.6875rem;
position: absolute;
top: -0.4375rem;
}
.main-navigation .menu .menu-item {
position: relative;
}
.main-navigation .menu .menu-item a {
background: #efefef;
border: 0.0625rem solid #dcdcdc;
color: #1450aa;
display: block;
padding: 0.809rem 1.618rem;
text-decoration: none;
}
.main-navigation .menu .menu-item a:hover,
.main-navigation .menu .menu-item a:focus {
background: #1450aa;
color: #efefef;
}
.main-navigation .menu .menu-item a:active {
background: #0b3d86;
color: #efefef;
}
.main-navigation .sub-menu {
display: none;
border-left: 0.1875rem solid #dcdcdc;
border-right: 0.1875rem solid #dcdcdc;
}
.menu .menu-item-has-children,
.sub-menu .menu-item-has-children {
position: relative;
}
.main-navigation .menu .menu-item-has-children a {
padding: 0.809rem 3.4375rem 0.809rem 1.618rem;
}
/* Hide/Show Main Menu if checkbox unchecked/checked */
.site-header > [type="checkbox"]:checked ~ .main-navigation {
display: block;
}
/* Hide/Show Sub-Menu if checkbox unchecked/checked */
.main-navigation [type="checkbox"]:checked ~ ul {
display: block;
}
/* Toggle buttons */
.top-nav-label {
cursor: pointer;
display: block;
font-size: 0;
margin: 0;
overflow: hidden;
padding: 0;
}
.top-nav-label .open-close-menu {
display: block;
position: absolute;
z-index: 99999;
}
.top-nav-label .open-close-menu::before,
.top-nav-label .open-close-menu::after {
border-radius: 0.1875rem;
content: "";
display: block;
position: absolute;
}
/* Hamburger button */
.site-header > .top-nav-label {
position: relative;
right: 0;
top: 0;
height: 2.5rem;
width: 2.5rem;
border-radius: 0.1875rem;
}
.site-header > .top-nav-label .open-close-menu {
background: #1450aa;
border-radius: 0.1875rem;
height: 0.25rem;
left: 0.1875rem;
right: 0.1875rem;
top: 1.125rem;
transition: 0.25s ease-in-out 0s;
}
.site-header > .top-nav-label .open-close-menu::before,
.site-header > .top-nav-label .open-close-menu::after {
background-color: #1450aa;
height: 0.25rem;
left: 0;
transition-duration: 0.3s, 0.3s;
width: 100%;
}
.site-header > .top-nav-label .open-close-menu::before {
top: -0.6875rem;
transform-origin: top;
}
.site-header > .top-nav-label .open-close-menu::after {
bottom: -0.6875rem;
transform-origin: bottom;
}
/* Hamburger animation */
.site-header > [type="checkbox"]:checked ~ .top-nav-label .open-close-menu {
background: none;
transition-delay: 0s, 0.3s;
}
.site-header
> [type="checkbox"]:checked
~ .top-nav-label
.open-close-menu::before,
.site-header
> [type="checkbox"]:checked
~ .top-nav-label
.open-close-menu::after {
left: 0.125rem;
transition-delay: 0s, 0.3s;
}
.site-header
> [type="checkbox"]:checked
~ .top-nav-label
.open-close-menu::before {
top: 0;
transform: rotate(45deg);
}
.site-header
> [type="checkbox"]:checked
~ .top-nav-label
.open-close-menu::after {
bottom: 0;
transform: rotate(-45deg);
}
/* Hamburger button hover/focus/active */
.site-header > .top-nav-label:hover,
.site-header > input[type="checkbox"]:focus ~ .top-nav-label,
.site-header > input[type="checkbox"]:hover ~ .top-nav-label {
background: #1450aa;
}
.site-header > .top-nav-label:hover .open-close-menu,
.site-header > .top-nav-label:hover .open-close-menu::after,
.site-header > .top-nav-label:hover .open-close-menu::before,
.site-header > input[type="checkbox"]:focus ~ .top-nav-label .open-close-menu,
.site-header
> input[type="checkbox"]:focus
~ .top-nav-label
.open-close-menu::before,
.site-header
> input[type="checkbox"]:focus
~ .top-nav-label
.open-close-menu::after,
.site-header > input[type="checkbox"]:hover ~ .top-nav-label .open-close-menu,
.site-header
> input[type="checkbox"]:hover
~ .top-nav-label
.open-close-menu::before,
.site-header
> input[type="checkbox"]:hover
~ .top-nav-label
.open-close-menu::after {
background: #efefef;
}
.site-header
> input[type="checkbox"]:checked
~ .top-nav-label:hover
.open-close-menu,
.site-header
> input[type="checkbox"]:checked:focus
~ .top-nav-label
.open-close-menu,
.site-header
> input[type="checkbox"]:checked:hover
~ .top-nav-label
.open-close-menu {
background: none;
}
/* Plus/Minus button */
.main-navigation .menu-item-has-children > .top-nav-label {
position: absolute;
right: 0.0625rem;
top: 0.0625rem;
height: 3.3125rem;
width: 3.3125rem;
}
.main-navigation .menu-item-has-children > .top-nav-label .open-close-menu {
height: 100%;
width: 100%;
}
.main-navigation
.menu-item-has-children
> .top-nav-label
.open-close-menu::after,
.main-navigation
.menu-item-has-children
> .top-nav-label
.open-close-menu::before {
background: #1450aa;
transition: transform 0.25s ease-out;
}
.main-navigation
.menu-item-has-children
> .top-nav-label
.open-close-menu::before {
height: 50%;
left: calc(50% - 0.1875rem / 2);
top: 25%;
width: 0.1875rem;
}
.main-navigation
.menu-item-has-children
> .top-nav-label
.open-close-menu::after {
height: 0.1875rem;
left: 25%;
top: calc(50% - 0.1875rem / 2);
width: 50%;
}
.main-navigation .menu-item-has-children a:hover ~ .top-nav-label,
.main-navigation .menu-item-has-children a:focus ~ .top-nav-label,
.main-navigation .menu-item-has-children a:active ~ .top-nav-label,
.main-navigation .menu-item-has-children:focus-within a ~ .top-nav-label {
background: #efefef;
}
/* Plus/Minus button animation */
.main-navigation
[type="checkbox"]:checked
~ .top-nav-label
.open-close-menu::before {
transform: rotate(90deg);
}
.main-navigation
[type="checkbox"]:checked
~ .top-nav-label
.open-close-menu::after {
transform: rotate(180deg);
}
/* Plus/Minus button hover/focus/active */
.main-navigation .menu-item-has-children > .top-nav-label:hover {
background: #1450aa;
}
.main-navigation
.menu-item-has-children
input[type="checkbox"]:focus
~ .top-nav-label {
background: #1450aa;
}
.main-navigation
.menu-item-has-children
> .top-nav-label:hover
.open-close-menu::after,
.main-navigation
.menu-item-has-children
> .top-nav-label:hover
.open-close-menu::before,
.main-navigation
.menu-item-has-children
input[type="checkbox"]:focus
~ .top-nav-label
.open-close-menu::before,
.main-navigation
.menu-item-has-children
input[type="checkbox"]:focus
~ .top-nav-label
.open-close-menu::after {
background: #efefef;
}
/* Hide and place checkbox over the toggle buttons */
.site-header input[type="checkbox"] {
cursor: pointer;
display: block;
opacity: 0;
position: absolute;
z-index: 2;
}
.site-header > input[type="checkbox"] {
right: 0.5rem;
top: 2.75rem;
}
.site-header .menu-item-has-children input[type="checkbox"] {
right: 1rem;
top: 1rem;
}
/* Display the right label if checkbox checked */
.site-header input[type="checkbox"] + .top-nav-label .close-menu,
.site-header input[type="checkbox"]:checked + .top-nav-label .open-menu {
display: none;
}
.site-header input[type="checkbox"]:checked + .top-nav-label .close-menu,
.site-header input[type="checkbox"] + .top-nav-label .open-menu {
display: block;
}
@media only screen and (min-width: 37.5em) {
.site-title {
font-size: 1.5rem;
}
}
@media only screen and (min-width: 64em) {
.site-branding {
flex: 1 0 30%;
max-width: 30%;
}
.site-title {
font-size: 1.8rem;
}
.site-header input[type="checkbox"],
.site-header .top-nav-label {
display: none;
}
.main-navigation {
display: block;
flex: 1 0 70%;
max-width: 70%;
}
.main-navigation .menu {
border: none;
display: flex;
justify-content: flex-end;
}
.main-navigation .menu::before,
.main-navigation .menu::after {
display: none;
}
.main-navigation .menu > .menu-item {
margin-left: 0.3125rem;
}
.main-navigation .menu .menu-item a {
background: #fff;
border: none;
color: #1450aa;
padding: 0.4045rem 0.809rem;
}
.main-navigation .menu .menu-item a:hover {
background: #fff;
color: #1450aa;
text-decoration: underline;
}
.main-navigation .menu .menu-item a:active {
color: #0b3d86;
text-decoration: none;
}
.main-navigation .menu-item-has-children > .top-nav-label {
position: absolute;
right: 0;
top: 0;
height: 2.5625rem;
width: 2.5625rem;
}
.main-navigation .menu-item-has-children a:hover ~ .top-nav-label,
.main-navigation .menu-item-has-children a:focus ~ .top-nav-label,
.main-navigation .menu-item-has-children a:active ~ .top-nav-label,
.main-navigation .menu-item-has-children:focus-within a ~ .top-nav-label {
background: #fff;
}
.main-navigation .menu .menu-item-has-children a {
padding: 0.4045rem 0.809rem;
}
.main-navigation .menu .menu-item-has-children > a::after {
display: inline-block;
content: "\25BC";
margin-left: 0.4045rem;
text-decoration: none;
position: relative;
}
/* Show submenu on hover/focus */
.main-navigation .menu-item-has-children > a:hover ~ ul,
.main-navigation .menu-item-has-children:hover > a ~ ul,
.main-navigation .menu-item-has-children > a ~ ul:hover,
.main-navigation .menu-item-has-children > a:focus ~ ul {
display: block;
z-index: 999;
pointer-events: auto;
}
/* Same rules - Focus-within not supported by Edge/IE. Unsupported selectors cause the entire block to be ignored, so we must repeat all styles separately. */
.main-navigation .menu-item-has-children:focus-within > a ~ ul {
display: block;
z-index: 999;
}
.main-navigation .sub-menu {
min-width: 100%;
position: absolute;
border: 0.0625rem solid #dcdcdc;
}
.main-navigation .sub-menu::before {
border-bottom: 0.5625rem solid #dcdcdc;
border-left: 0.5625rem solid transparent;
border-right: 0.5625rem solid transparent;
border-top: 0 solid #dcdcdc;
content: "";
left: 30%;
position: absolute;
top: -0.5625rem;
}
.main-navigation .sub-menu::after {
border-color: #fff transparent;
border-style: solid;
border-width: 0 0.5rem 0.5rem;
content: "";
left: calc(30% + 0.0625rem);
position: absolute;
top: -0.4375rem;
}
.main-navigation .sub-menu .sub-menu::before,
.main-navigation .sub-menu .sub-menu::after {
display: none;
}
.main-navigation .sub-menu .menu-item-has-children > a::after {
content: "\25C4";
vertical-align: top;
}
.main-navigation .sub-menu ul {
left: -100%;
top: 0;
max-width: 100%;
}
}
@media only screen and (min-width: 64em) and (any-pointer: coarse) {
.main-navigation .menu .menu-item-has-children a {
padding-right: 3rem;
}
.site-header .menu-item-has-children .top-nav-label {
display: block;
}
.site-header .menu-item-has-children input[type="checkbox"] {
display: block;
right: 0.625rem;
top: 0.625rem;
}
.main-navigation .menu-item-has-children > a:focus ~ ul,
.main-navigation
.menu-item-has-children
> input[type="checkbox"]:not(:checked):focus
~ ul {
display: none;
}
.main-navigation .menu .menu-item-has-children > a::after,
.main-navigation .sub-menu .menu-item-has-children > a::after {
display: none;
}
}
@media only screen and (min-width: 100em) {
.site {
max-width: 80rem;
}
.site-title {
font-size: 2rem;
}
.main-navigation .sub-menu ul {
left: 100%;
top: 0;
max-width: 100%;
}
.main-navigation .sub-menu .menu-item-has-children > a::after {
content: "\25BA";
}
}
Sinon, les sources (HTML + CSS) sont disponibles sur Github et Gitlab. Vous pouvez également voir un aperçu sur Codepen.
3 commentaires
L’hypothèse est correcte, donc pas si perdu ! Un
label
est associé à uninput
par un attributid
. Si plusieurs input possèdent le même id, le label n’agira que sur le premier input rencontré (en partant du haut du document) possédant cet id. Concernant le CSS, la subtilité vient de~
: cela ne concerne que la liste (ul) après la case à cocher. Donc, ce code est générique ; il fonctionnera pour toute liste proche d’une case cochée.
Pour chaque sous-menu, il est donc essentiel d’utiliser un id différent. De même, l’attributaria-labelledby
sert à associer un élément à un autre par son id. Ici, il n’est pas forcément nécessaire (voire même redondant) tant que l’associationlabel
etinput
est correcte.
Une solution simple est d’ajouter un numéro pour chaque id/name:< input type="checkbox" id="submenu-toggle1" name="submenu-toggle1" class="submenu-toggle" />< label for="submenu-toggle1" class="top-nav-label" id="submenu-toggle-label1">
et d’incrémenter ce numéro à chaque nouvelle utilisation. (note: j’ai ajouté des espaces avant les<
pour éviter que ça ne se transforme, il faut les retirer)
Attention également, l'icône "hamburger" n'est pas visible sur mobile. Il manque lebackground
et les pseudo-éléments::before
et::after
dans le CSS. (tout du moins ça n'apparaît pas sur Firefox en simulation mobile)
Bonjour,
Merci pour cette aide précieuse !
Mais j’ai un petit souci avec le code que je ne parviens pas à corriger.
Quand je remplace le “Page 2 with children” par un mot beaucoup plus court (dans mon premier cas, “Régions”), les ‘subpages’ ont un retour à la ligne dans le texte, encore plus important avec les sous-sous-menus lorsque c’est affiché en mode large / PC (dans Chrome et Edge). Je me demande si ce n’est pas un % du parent.
Comment puis-je améliorer cela ?
D’avance merci !
Gérald
Anthony S.
Bonsoir,
Merci pour ce tutoriel fort clair!
Quand on met deux sous-menus, sur smartphone cela active désactive uniquement le premier déclaré. J’essaie de comprendre, ce doit être une histoire d’ID mais pas sûr car ce code :
/* Hide/Show Sub-Menu if checkbox unchecked/checked */
.main-navigation [type=”checkbox”]:checked ~ ul {
display: block;
}
semble indiquer qu’on active/désactive si le composant est de type case à cocher.
Bon vous aurez compris je suis perdu 🙁
Merci pour votre temps!
ah voici un lien pour tester sur smartphone :
https://fourmisenfolie.yj.fr/CACDS/