Mega menu pas à pas : HTML, CSS et JavaScript accessibles
Comment créer un mega menu : code HTML, CSS et JavaScript à copier, prise en charge du clavier et des lecteurs d’écran, version téléphone, erreurs fréquentes et démo.
Un mega menu est un grand panneau qui se déploie depuis le menu du haut et affiche plusieurs groupes de liens à la fois, souvent accompagnés de courtes descriptions. Vous le connaissez des boutiques en ligne et des sites d’entreprises à l’offre étendue. Bien conçu, il raccourcit le chemin du client vers un service précis. Mal conçu, il se referme avant que le curseur l’atteigne, ne fonctionne pas sur téléphone et ne peut pas être utilisé au clavier.
Dans ce guide, nous allons construire un mega menu de zéro : du HTML, du CSS et environ 30 lignes de JavaScript, sans aucune bibliothèque. J’ai testé le code dans un navigateur, et vous pouvez ouvrir la démo fonctionnelle ici. Le même modèle fonctionne dans mon propre menu : le bouton « Outils gratuits » en haut de cette page.
Quand un mega menu a du sens
Un mega menu est utile lorsque :
- une partie de votre offre compte plus de 7 à 8 pages, par exemple plusieurs types de services répartis en plusieurs groupes ;
- votre offre se divise naturellement en catégories (boutique, entreprise du bâtiment, cabinet comptable proposant des services à différents types de clients) ;
- vous voulez afficher une courte description à côté des liens ou un appel à l’action mis en avant.
Si l’ensemble du site compte 5 à 6 pages, un menu ordinaire suffit. Un mega menu de trois liens paraît vide et ne fait qu’alourdir le code.
Comment un bon mega menu doit fonctionner
Avant d’écrire du code, fixez les règles. Chacune a sa raison d’être :
- Il s’ouvre au clic, et au clavier avec la touche Entrée ou la barre d’espace. Le simple survol de la souris ne fonctionne pas sur téléphone.
- Il se ferme lorsqu’on clique à côté, avec la touche Échap (Escape), et lorsqu’on passe plus loin avec la touche Tab.
- Le lecteur d’écran sait si le panneau est déployé, grâce à l’attribut
aria-expanded. - Les liens sont de simples balises
<a>en HTML, si bien que Google voit toutes les pages. - Il n’y a pas d’interstice entre le bouton et le panneau, dans lequel le curseur « perdrait » le menu.
- Sur téléphone, le panneau devient une liste déroulante sous le bouton « Menu ».
- Les groupes ont des titres, et l’ensemble du panneau comporte au plus un appel à l’action mis en avant.

Étape 1 : HTML
Chaque entrée déroulante se compose d’un bouton et d’un panneau. Un bouton et non un lien, car il n’ouvre rien à une nouvelle adresse, il ne fait qu’afficher le panneau. L’attribut aria-controls désigne le panneau, et hidden le masque au départ :
<header class="top">
<div class="top__in">
<a class="logo" href="/">Pomost</a>
<button class="burger" type="button" aria-expanded="false" aria-controls="menu">Menu</button>
<nav class="nav" id="menu" aria-label="Menu główne">
<ul class="nav__list">
<li>
<button class="nav__toggle" type="button" aria-expanded="false" aria-controls="mm-oferta">Oferta</button>
<div class="mega" id="mm-oferta" hidden>
<div class="mega__in">
<div>
<p class="mega__title" id="mm-tarasy">Tarasy</p>
<ul aria-labelledby="mm-tarasy">
<li><a href="/tarasy-kompozytowe">Tarasy kompozytowe<small>Bez malowania, na lata</small></a></li>
<li><a href="/tarasy-drewniane">Tarasy drewniane<small>Modrzew, sosna, egzotyki</small></a></li>
</ul>
</div>
<!-- autres colonnes : Ogrody (jardins), Dodatki (accessoires) -->
<div class="mega__promo">
<b>Bezpłatny pomiar</b>
<p>Przyjedziemy, zmierzymy i w 2 dni prześlemy wycenę.</p>
<a href="/kontakt">Umów pomiar</a>
</div>
</div>
</div>
</li>
<li><a href="/realizacje">Realizacje</a></li>
<li><a href="/kontakt">Kontakt</a></li>
</ul>
</nav>
</div>
</header>
Le titre du groupe est un simple paragraphe, et la liste lui est reliée par aria-labelledby. Le lecteur d’écran annonce alors « Tarasy, liste, 3 éléments », et la structure des titres de la page n’est pas encombrée d’une douzaine de H2 supplémentaires.
Je rencontre souvent role="menu" et role="menuitem" dans ce genre de menus. C’est une erreur : ces rôles sont destinés aux menus d’application (comme « Fichier, Édition » dans un éditeur) et promettent une prise en charge des flèches que le site ordinaire n’offre pas. Pour la navigation, les recommandations WAI-ARIA Authoring Practices conseillent justement le modèle « disclosure », c’est-à-dire un bouton qui affiche et masque un contenu.
Étape 2 : CSS
Trois choses comptent avant tout : le panneau est collé à l’en-tête sans interstice, l’aspect du bouton dépend de aria-expanded, et le focus au clavier est bien visible :
.top { position: relative; background: #fff; }
.nav__list { display: flex; gap: 4px; margin: 0; padding: 0; list-style: none; }
.nav__toggle { display: inline-flex; align-items: center; gap: 8px; padding: 10px 14px;
border: 0; border-radius: 10px; background: none; font: inherit; cursor: pointer; }
/* la flèche pivote lorsque le panneau est ouvert : l’état est lu directement depuis aria-expanded */
.nav__toggle::after { content: ""; width: 7px; height: 7px; border-right: 2px solid currentColor;
border-bottom: 2px solid currentColor; transform: translateY(-2px) rotate(45deg); transition: transform .2s; }
.nav__toggle[aria-expanded="true"]::after { transform: translateY(1px) rotate(-135deg); }
/* panneau pleine largeur, qui commence exactement sous l’en-tête (top: 100%, sans marge) */
.mega { position: absolute; left: 0; right: 0; top: 100%; z-index: 10; background: #fff;
box-shadow: 0 24px 40px -24px rgba(17, 21, 28, .25); }
.mega__in { max-width: 1120px; margin: 0 auto; padding: 28px 20px 32px;
display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)) minmax(0, 1.2fr); gap: 32px; }
.mega ul { margin: 0; padding: 0; list-style: none; }
.mega ul a { display: block; padding: 8px 10px; border-radius: 10px; text-decoration: none; }
.mega ul small { display: block; color: #5b6472; font-size: 13.5px; }
:focus-visible { outline: 3px solid #2f5bff; outline-offset: 2px; }
/* courte animation d’entrée du panneau, sauf pour les personnes qui ont désactivé les animations dans leur système */
@media (prefers-reduced-motion: no-preference) {
.mega:not([hidden]) { animation: wjazd .18s ease-out; }
}
@keyframes wjazd { from { opacity: 0; transform: translateY(-6px); } }
Remarquez minmax(0, 1fr) dans la grille. Sans cela, un mot long dans une colonne peut élargir le panneau au-delà de l’écran.
Étape 3 : JavaScript
Le script fait quatre choses : il ouvre et ferme le panneau au clic, ferme les autres panneaux, gère la touche Échap (Escape) et ferme le menu lorsque vous cliquez à côté ou que vous sortez du menu avec la touche Tab :
const nav = document.querySelector('.nav');
const toggles = [...nav.querySelectorAll('.nav__toggle')];
function setOpen(btn, open) {
btn.setAttribute('aria-expanded', String(open));
document.getElementById(btn.getAttribute('aria-controls')).hidden = !open;
}
function closeAll(except) {
toggles.forEach((b) => { if (b !== except) setOpen(b, false); });
}
toggles.forEach((btn) => btn.addEventListener('click', () => {
const open = btn.getAttribute('aria-expanded') !== 'true';
closeAll(btn);
setOpen(btn, open);
}));
// Escape ferme le panneau ouvert et revient à son bouton
nav.addEventListener('keydown', (e) => {
const btn = toggles.find((b) => b.getAttribute('aria-expanded') === 'true');
if (e.key === 'Escape' && btn) { setOpen(btn, false); btn.focus(); }
});
// un clic en dehors du menu, ou le passage du focus à un élément extérieur au menu, ferme le panneau
document.addEventListener('click', (e) => { if (!nav.contains(e.target)) closeAll(); });
document.addEventListener('focusin', (e) => { if (!nav.contains(e.target)) closeAll(); });
L’état du menu est stocké à un seul endroit : dans les attributs aria-expanded et hidden. Le CSS le lit pour faire pivoter la flèche, le lecteur d’écran pour annoncer « développé » et « réduit », et le script pour décider quoi fermer. Il n’y a pas de classes distinctes du type is-open, qui pourraient se désynchroniser de ce qu’entend une personne aveugle.
Une anecdote issue des tests : la première version fermait le menu sur l’événement focusout. Cela fonctionnait au clavier, mais le panneau disparaissait lorsqu’on cliquait sur son arrière-plan, car le navigateur retire alors le focus du bouton. L’événement focusin sur l’ensemble du document ne réagit que lorsque le focus arrive réellement sur un autre élément ; ce problème n’existe donc pas.
Étape 4 : la version téléphone
Sur téléphone, il n’y a pas de place pour quatre colonnes. Nous transformons le même HTML en liste déroulante : la navigation se cache derrière le bouton « Menu », et les panneaux s’empilent les uns sous les autres :
.burger { display: none; }
@media (max-width: 860px) {
.burger { display: inline-flex; margin-left: auto; padding: 10px 14px;
border: 1px solid #e3e6eb; border-radius: 10px; background: none; font: inherit; }
.nav { display: none; position: absolute; left: 0; right: 0; top: 100%; z-index: 10;
max-height: calc(100dvh - 68px); overflow-y: auto; background: #fff; }
.nav.is-open { display: block; }
.nav__list { flex-direction: column; padding: 8px 12px 16px; }
.nav__toggle { width: 100%; justify-content: space-between; padding: 14px 12px; }
.mega { position: static; box-shadow: none; }
.mega__in { grid-template-columns: minmax(0, 1fr); gap: 20px; padding: 8px 12px 16px; }
}
const burger = document.querySelector('.burger');
burger.addEventListener('click', () => {
const open = burger.getAttribute('aria-expanded') !== 'true';
burger.setAttribute('aria-expanded', String(open));
nav.classList.toggle('is-open', open);
});
max-height avec l’unité dvh et overflow-y: auto permettent de faire défiler un long menu sur un petit écran, sans que la barre d’adresse du navigateur du téléphone le masque.

Comment tester le menu en 5 minutes
Avant de publier le menu, testez-le comme les gens l’utiliseront :
- Clavier : avec la touche Tab, atteignez « Oferta » (Offre) ; Entrée ouvre le panneau, les Tab suivants parcourent les liens, Échap ferme le panneau et revient au bouton. Un Tab après le dernier lien ferme le panneau.
- Souris : déplacez le curseur du bouton vers le panneau en diagonale, puis cliquez sur un espace vide du panneau. Le panneau doit rester ouvert.
- Lecteur d’écran : dans NVDA (Windows) ou VoiceOver (Mac, iPhone), le bouton doit être annoncé « Oferta, bouton, réduit », puis « développé » après ouverture.
- Téléphone et zoom : avec une largeur de 320 px et un zoom à 200 %, rien ne doit dépasser de l’écran ni obliger à faire défiler latéralement.
- Lighthouse dans Chrome (onglet Accessibilité) repérera les noms de boutons manquants et les contrastes trop faibles.
L’état général de votre site, y compris l’accessibilité, le SEO et la vitesse, se vérifie en 30 secondes avec mon outil Analyser votre site.
Les erreurs les plus fréquentes dans un mega menu
- Un menu qui s’ouvre uniquement au survol. Impossible de l’ouvrir sur téléphone, et sur ordinateur il s’ouvre par accident.
- Un interstice entre le bouton et le panneau. Un simple
margin-top: 8pxsuffit pour que le curseur sorte du menu en route vers le panneau et que tout se referme. <a href="#">au lieu d’un bouton. La page remonte en haut, et le lecteur d’écran annonce un lien qui ne mène nulle part.role="menu"sans gestion des flèches. Une promesse que le site ne tient pas est pire que l’absence d’ARIA.- Des liens chargés par script seulement au survol. Google risque de ne pas les voir.
- Trop de liens. Un panneau de 120 entrées est un catalogue, pas une navigation. Le client cherche 2 à 3 secondes et, s’il ne trouve pas, retourne sur Google.
- Un panneau plus large que l’écran sur téléphone. Symptôme classique : la page peut être déplacée latéralement de quelques pixels.
Et avec WordPress ?
Dans WordPress, le mega menu est généralement fourni par le thème ou par une extension. Avant de la conserver, passez la même liste de tests : beaucoup d’extensions n’ouvrent le menu qu’au survol, ne définissent pas aria-expanded et ajoutent plusieurs dizaines de kilo-octets de scripts sur chaque page. Il est parfois plus simple et plus rapide d’ajouter son propre menu à partir du code de ce guide.
En résumé
Un bon mega menu, c’est un bouton avec aria-expanded, un panneau avec hidden, de simples liens en HTML et trois façons de le fermer : un clic à côté, Échap et Tab. Sur téléphone, le même code fonctionne comme une liste déroulante. Le code complet de cet article se trouve dans la démo : ouvrez-la, testez-la à la souris et au clavier, puis examinez le code source de la page.
Si votre site propose une offre étendue et que le menu ne suit plus, écrivez-moi. Je reconstruirai la navigation de votre site actuel ou je concevrai un nouveau site vitrine. Je réponds sous 24 heures, et les nouveaux clients bénéficient de 20 % de remise sur la première commande.
Questions fréquentes
Non, à condition que les liens figurent dans le code HTML ordinaire de la page et ne soient pas chargés seulement au survol de la souris. Google voit alors toutes les pages. N’exagérez toutefois pas le nombre de liens : une douzaine d’entrées bien nommées aident, alors que plusieurs centaines diluent le signal indiquant quelles pages sont les plus importantes.
Mieux vaut un clic. Le survol ne fonctionne pas sur les écrans tactiles, et sur ordinateur le menu s’ouvre par accident lorsque le curseur ne fait que traverser l’en-tête. Si vous tenez au survol, ajoutez un délai d’environ 200 ms et gardez le clic comme mode d’ouverture principal.
Le rôle menu est destiné aux menus d’application, comme « Fichier, Édition » dans un éditeur de texte, et exige une prise en charge des touches fléchées. Pour la navigation d’un site, les recommandations WAI-ARIA conseillent un modèle plus simple : un bouton avec aria-expanded, qui affiche et masque une liste de liens ordinaires.
Une bonne règle : 3 à 4 colonnes et 3 à 8 liens dans chacune. Si vous en avez besoin de davantage, c’est le signe qu’il vaut mieux réorganiser votre offre ou ajouter une page de catégorie vers laquelle le menu renvoie.
Services liés au sujet
Vous avez un projet similaire ?
Décrivez votre besoin et je vous envoie un devis sous 24 heures. Les nouveaux clients bénéficient de 20 % de réduction sur la première commande.


