Méga menu avec CSS Grid : survol, focus, mobile et bases de l’accessibilité

Cet article vous a aidé ?
Front-end orienté production

Méga menu avec CSS Grid : survol, focus, mobile et bases de l’accessibilité

Votre équipe marketing veut un « méga menu » “simple”. Puis un utilisateur clavier ne peut pas atteindre la moitié des liens, les taps sur mobile ouvrent-et-ferment comme un ascenseur cassé,
et quelqu’un ouvre un ticket intitulé « le nav disparaît quand je souffle dessus ».

Voici comment construire un méga menu qui se comporte comme un composant mature : survol et focus prévisibles, comportement mobile sensé, et bases d’accessibilité qui ne vous feront pas honte lors d’un audit.

Ce qu’est un méga menu (et ce que ce n’est pas)

Un méga menu est un patron de navigation où un élément de premier niveau ouvre un grand panneau contenant plusieurs groupes de liens — souvent disposés en colonnes,
parfois avec du contenu mis en avant, des titres, des icônes, des promos ou des raccourcis « populaires ». Le mot clé est groupe. Un méga menu existe pour exposer
beaucoup de destinations sans entraîner l’utilisateur dans un labyrinthe de clics.

Ce que ce n’est pas : une poubelle pour chaque URL que votre organisation a jamais créée. Si votre méga menu ressemble à un export CSV, le problème est l’architecture
de l’information, pas le CSS.

Aussi : un méga menu n’est pas un « menu » au sens ARIA de role="menu" à moins que vous ne construisiez des widgets de type application.
La plupart des sites veulent une navigation normale : des listes de liens dans un <nav>, avec un bouton de divulgation qui affiche un panneau.
Ne faites pas aux utilisateurs d’assistance technique l’offrande de leur faire croire qu’ils sont dans une application de bureau.

Règle opinionnée

Si votre panneau méga nécessite plus d’un défilement sur un écran d’ordinateur, ce n’est plus un méga menu. C’est un plan du site avec un déclencheur au survol.
Corrigez la taxonomie ou ajoutez une page « Tous les produits » dédiée.

Faits intéressants et contexte historique

Les méga menus n’ont pas émergé parce que les designers ont collectivement choisi la complexité. Ils sont une réponse à l’échelle : plus de sections, plus de gammes de produits,
plus d’acquisitions, et le fantasme persistant que la navigation peut compenser l’entropie organisationnelle.

Fait 1 : Les premiers patrons de « méga menu » sont devenus courants au milieu des années 2000 quand les sites ont tenté de réduire les cascades de flyout à plusieurs niveaux difficiles à viser.
Fait 2 : CSS Grid (support navigateur vers 2017) a finalement facilité la construction de mises en colonnes robustes sans empiler des floats ou des calculs fragile d’inline-block.
Fait 3 : La navigation dépendante du survol a toujours été hostile aux appareils tactiles ; la navigation mobile généralisée a rendu les patrons de divulgation et le « cliquer pour ouvrir » normaux.
Fait 4 : Le bug du « gap au survol » — où un panneau se ferme quand le pointeur passe du déclencheur au panneau — était historiquement contourné avec du padding invisible et des timers JS. Nous utilisons toujours le padding ; on prétend juste que c’est élégant.
Fait 5 : ARIA aria-expanded pour les contrôles de divulgation (boutons qui ouvrent des panneaux) est l’un des attributs les plus informatifs que vous pouvez ajouter pour les lecteurs d’écran. Il leur dit ce qui s’est passé.
Fait 6 : :focus-within (largement utilisable depuis la fin des années 2010) a donné au CSS un moyen pratique de garder les dropdowns ouverts tant que le focus clavier est à l’intérieur, sans JS.
Fait 7 : prefers-reduced-motion (adoption vers 2019) a transformé « animations partout » en conversation d’accessibilité, pas seulement de performance.
Fait 8 : Les patrons de « focus trap » ont été popularisés par les modales. Les équipes les ont parfois appliqués aux tiroirs de navigation, souvent incorrectement, créant une navigation qui ressemble à une pièce verrouillée.
Fait 9 : L’attribut inert est désormais suffisamment supporté pour être utile pour désactiver le contenu d’arrière-plan pendant des overlays, mais ce n’est pas une excuse pour sauter les tests clavier.
« Tout échoue, tout le temps. » — Werner Vogels

Cette citation parle des systèmes distribués, mais elle s’applique aussi aux composants UI. Votre méga menu échoue sur un appareil que vous n’avez pas testé, avec un mode d’entrée oublié,
dans une langue qui a fait replier vos colonnes, derrière une bannière qui a changé votre contexte d’empilement. Prévoyez l’échec. Concevez le chemin ennuyeux.

Architecture : le balisage d’abord, puis CSS Grid

La façon la plus rapide de construire un méga menu inaccessible est de démarrer dans le CSS. La deuxième façon la plus rapide est de démarrer dans le JavaScript. Commencez par la sémantique :
une liste de liens. Ajoutez ensuite un contrôle de divulgation pour révéler plus de liens. Puis stylisez. Ensuite ajoutez juste assez de JS pour gérer le mobile et l’état.

Le balisage que vous voulez réellement

Pour un méga menu de site web, vous souhaitez généralement cette structure :

  • <nav aria-label="Primary"> pour la région.
  • <ul><li> pour les éléments de premier niveau.
  • Un <button> pour basculer un panneau (meilleur pour le comportement d’ouverture/fermeture), ou un lien si l’élément navigue.
  • Un conteneur de panneau (souvent un <div>) qui contient titres et groupes de liens.

Si l’élément de premier niveau navigue et ouvre un panneau, choisissez un comportement. « Il fait les deux » devient « il ne fait rien » pour les utilisateurs clavier.
Le compromis courant : élément de premier niveau lien, plus un bouton de divulgation distinct à côté.

Possession de l’état : CSS pour survol/focus, JS pour tap

Utilisez le CSS pour :

  • Ouverture au survol sur pointeurs fins (@media (hover:hover))
  • Garder ouvert tant que le focus est à l’intérieur (:focus-within)

Utilisez le JavaScript pour :

  • Basculer ouverture/fermeture sur tap/clic sur petits écrans
  • Mettre à jour aria-expanded et éventuellement hidden
  • Fermer sur Échap
  • Fermer lors d’un clic à l’extérieur (prudemment ; ne bloquez pas les clics légitimes)

Exactement une petite blague (1/2)

Si votre menu nécessite une machine à états de 200 lignes, félicitations : vous avez construit un système distribué, mais pour la désillusion.

Comportement survol et focus sans scintillement

Le survol est une commodité, pas une fondation. Il doit être additif : agréable pour les utilisateurs souris, sans importance pour le tactile, et jamais la seule façon de révéler des liens.
Les deux bugs que vous allez livrer si vous n’êtes pas prudent :

  • Scintillement : le menu s’ouvre puis se ferme lorsque le pointeur se déplace.
  • Perte de focus : les utilisateurs clavier tabulent sur le déclencheur, le panneau s’ouvre, puis se replier quand le focus se déplace dans le panneau.

Corriger la perte de focus avec :focus-within

Le sélecteur CSS le plus utile dans tout ce projet est :focus-within. Appliquez-le à l’élément de liste qui contient à la fois le déclencheur et le panneau.
Quand un descendant est focalisé, le panneau reste ouvert.

Dans la démo en haut, cette règle fait le travail :

cr0x@server:~$ cat snippets/nav.css | sed -n '1,18p'
.nav > ul > li:hover > .panel,
.nav > ul > li:focus-within > .panel{
  display:block;
}

.panel::before{
  content:"";
  position:absolute;
  left: 0;
  top: -10px;
  height: 10px;
  width: 100%;
}

Le pont ::before est old-school, et ça marche. Il crée une « zone sûre » pour que le pointeur puisse passer du déclencheur au panneau sans quitter la zone de survol.
Sans cela, vous recevrez des rapports « le menu disparaît quand j’essaie de l’utiliser ». Ces rapports sont exacts.

N’ouvrez pas au survol pour le tactile

Les navigateurs tactiles émulent parfois le survol d’une manière qui n’enchante personne. Utilisez des media queries pour restreindre le comportement de survol :

  • @media (hover:hover) and (pointer:fine) → comportement de survol autorisé.
  • @media (hover:none) → reposez-vous sur un basculement explicite.

Hacks de temporisation : évitez sauf nécessité

L’approche classique est d’ajouter un délai de fermeture (par exemple 150ms) pour que de légères dérives du pointeur ne ferment pas le menu. Ça fait du bien… jusqu’à ce que non.
Les délais peuvent rendre l’UI collante et introduire des instabilités que l’automatisation de tests amplifie.

Préférez des correctifs géométriques (padding pont, placement sensé du panneau, taille de déclencheur adéquate) plutôt que des timers. Les timers sont le dernier recours.

Comportement mobile : tap, défilement et « ne me bloquez pas »

Le mobile est l’endroit où les méga menus vont mourir. Pas parce que c’est impossible, mais parce que les équipes tentent de préserver le comportement desktop au lieu de rencontrer la plateforme à mi-chemin.
Sur mobile, un méga menu est généralement soit :

  • un accordéon rétractable dans un tiroir de nav, ou
  • une liste empilée où taper une section révèle les liens groupés en ligne.

Choisissez un modèle et tenez-vous y

Voici deux modèles qui fonctionnent en production :

Modèle Ce que ça donne Quand l’utiliser
Accordéon en ligne Les sections se développent vers le bas ; pas d’overlay. Quand la navigation supérieure est déjà en flux et que vous voulez zéro drame de blocage de défilement.
Tiroir de nav + accordéon Le hamburger ouvre un tiroir ; les sections se déploient à l’intérieur. Quand vous avez besoin d’espace et que vous souhaitez cacher la complexité.

Blocage du défilement : le piège le plus courant

Si vous utilisez un overlay/tiroir, vous pourriez bloquer le défilement du body. Faites-le prudemment ou vous déclencherez des bugs iOS Safari, la rupture du comportement « revenir en haut »,
ou des sauts étranges au moment de la fermeture. Si votre nav n’exige pas strictement un overlay, évitez totalement le blocage du défilement. Le meilleur blocage est celui que vous n’avez pas implémenté.

Comportements de fermeture qui respectent les personnes

  • Échap ferme le panneau/tiroir ouvert.
  • Taper à l’extérieur ferme (mais seulement quand c’est un overlay ; les accordéons en ligne ne doivent pas se refermer parce que vous avez tapé ailleurs).
  • Le focus doit se déplacer dans le contenu ouvert si vous ouvrez un tiroir, et revenir au déclencheur quand vous fermez.

Réduction des animations et performance

Si vous animez le panneau, gardez-le subtil et rapide. Et conditionnez-le :

  • Respectez prefers-reduced-motion: reduce.
  • Évitez d’animer des propriétés de layout qui causent des reflows (comme height depuis auto). Favorisez opacity et transform si vous devez animer.

Exactement une petite blague (2/2)

« Ajoutez juste un flou derrière le menu » est la façon de transformer un problème de navigation en programme de benchmarking GPU.

Bases de l’accessibilité : rôles, étiquettes et attentes

L’accessibilité n’est pas une ambiance. C’est un contrat : les utilisateurs clavier doivent atteindre tout, les lecteurs d’écran doivent comprendre les changements d’état, et tout le monde doit pouvoir se retirer.
« Ça marche sur mon trackpad » n’est pas un test qui passe.

Utilisez la sémantique de navigation, pas les menus d’application

Pour un en-tête de site, tenez-vous à :

  • <nav aria-label="Primary">
  • <ul><li><a> pour les liens
  • <button aria-expanded aria-controls> pour la divulgation

Évitez role="menu" à moins de construire un véritable widget de menu avec navigation par flèches et rôles menuitem. Si vous ajoutez des rôles de menu, vous héritez de ces règles d’interaction.
La plupart des équipes ajoutent les rôles et sautent les comportements. C’est pire que de ne rien faire.

Boutons de divulgation : ARIA minimum viable

Un bouton qui ouvre un panneau doit avoir :

  • aria-expanded="false|true" reflétant l’état
  • aria-controls="panel-id" pointant vers l’élément du panneau
  • Un nom accessible (« Produits », pas « Chevron »)

Le panneau lui‑même peut être un conteneur simple. S’il est toujours dans le DOM, vous pouvez utiliser hidden lorsqu’il est replié, ce qui le retire de l’arbre d’accessibilité et de l’ordre de tabulation.
Évitez les états « visuellement cachés mais toujours focusables » ; c’est ainsi que vous amenez les utilisateurs clavier à tabuler dans le vide.

Gestion du focus : à quoi ressemble le « bien »

Pour les menus hover/focus de bureau :

  • Tab vers le déclencheur : le panneau s’ouvre.
  • Tab se déplace dans les liens du panneau : le panneau reste ouvert.
  • Shift+Tab revient au déclencheur : le panneau reste ouvert tant que le focus est à l’intérieur du composant.

Pour les tiroirs mobiles :

  • L’ouverture du tiroir déplace le focus vers le premier élément focalisable à l’intérieur.
  • La fermeture rend le focus au bouton hamburger.
  • Le contenu d’arrière-plan n’est pas focalisable pendant que le tiroir est ouvert (utilisez inert ou un focus trap, mais implémentez correctement).

Zones tactiles et espacement

Les titres et liens de votre méga menu ne doivent pas être minuscules. La physique du « fat-finger » est imbattable. Donnez du padding aux liens ; ce n’est pas de l’espace gaspillé, c’est un budget d’erreur.
De plus, ne placez pas plusieurs petits contrôles (lien + chevron + badge) dans une rangée de 32px de hauteur et appelez ça « épuré ».

Patrons CSS Grid pour les panneaux méga

Grid est l’outil adapté car les panneaux méga sont des mises en page bidimensionnelles : colonnes de groupes, parfois avec blocs mis en avant, parfois avec images.
Flexbox convient pour l’alignement unidimensionnel ; il devient du ruban adhésif quand vous avez besoin de colonnes stables qui ne s’effondrent pas à certaines largeurs.

Patron 1 : colonne mise en avant fixe + colonnes de liens fluides

Un patron courant : un bloc « en vedette » (description, CTA, peut-être une image) et deux colonnes de liens. Utilisez :

  • grid-template-columns: 1.4fr 1fr 1fr au bureau
  • collapse en 1fr sur mobile

Patron 2 : auto-fit pour un nombre inconnu de groupes

Si votre CMS peut émettre 3–8 groupes et que vous ne le contrôlez pas strictement (un signe, mais courant), utilisez :
repeat(auto-fit, minmax(180px, 1fr)).

Cela permet aux colonnes de se renvoyer proprement sans vous forcer à coder des points de rupture pour chaque variation de contenu. Ce n’est pas magique : les titres longs se replient toujours.
Mais cela se dégrade professionnellement, pas de manière surprenante.

Patron 3 : garder les titres avec leurs premiers liens

Le bug sournois de mise en page : un titre de groupe en bas d’une colonne et ses liens en haut de la suivante après un wrapping. Résolvez-le en faisant de chaque groupe un seul élément de grille :
le titre et sa liste appartiennent au même conteneur.

Contextes d’empilement et le problème « pourquoi c’est derrière l’en-tête »

Les méga menus « disparaissent » souvent derrière des bannières, des en-têtes collants ou des sections hero. Ce n’est généralement pas que le z-index ;
ce sont les contextes d’empilement créés par :

  • position + z-index sur des ancêtres
  • transform sur des ancêtres (crée un nouveau contexte d’empilement)
  • filter, opacity, mix-blend-mode de la même manière

Quand vous déboguez cela, n’augmentez pas aléatoirement le z-index à 999999. C’est ainsi que vous obtenez un site où tout est au-dessus de tout, pour toujours.
Trouvez le contexte d’empilement et corrigez la racine.

Tâches pratiques : commandes, sorties et décisions

Le menu est du frontend, mais le livrer de façon fiable reste du travail système : testez, mesurez, surveillez, et ne faites pas confiance qu’à vos yeux.
Ci‑dessous des tâches pratiques à exécuter localement ou en CI pour attraper les désastres habituels. Chaque tâche inclut une commande, ce que signifie la sortie, et la décision suivante.

Tâche 1 : Confirmer les cibles de support navigateur (sanité de base)

cr0x@server:~$ cat package.json | jq '.browserslist'
[
  "defaults",
  "not IE 11",
  "maintained node versions"
]

Signification : Vous ne prétendez pas supporter IE 11. Bien ; Grid et les sélecteurs modernes n’auront pas besoin de hacks.
Décision : Si IE 11 doit être supporté (rare mais pas disparu), arrêtez-vous et redesign : vous ne construisez pas le même méga menu.

Tâche 2 : Lancer une build locale et vérifier que le CSS est bien livré

cr0x@server:~$ npm run build
> web@1.0.0 build
> vite build

vite v5.0.0 building for production...
dist/assets/index-3f2c7f1a.css  42.31 kB │ gzip: 8.90 kB
dist/assets/index-b0d1c8ad.js   182.12 kB │ gzip: 58.70 kB
✓ built in 2.54s

Signification : Le CSS existe, n’est pas suspectement minuscule, et est bien bundlé.
Décision : Si le CSS est absent ou trop petit, vérifiez votre pipeline de build pour un purge/tree-shaking supprimant les styles de nav (courant quand les noms de classes sont générés).

Tâche 3 : Détecter une suppression accidentelle des styles de focus

cr0x@server:~$ rg -n "outline:\s*none" dist/assets/index-*.css | head
1221:.nav-link:focus-visible{outline:none;box-shadow:0 0 0 3px rgba(122,162,255,.45);background:rgba(255,255,255,.06)}

Signification : L’outline est supprimé mais remplacé par un indicateur de focus visible (box-shadow).
Décision : Si vous trouvez outline:none sans remplacement, rejetez le changement. Les utilisateurs clavier déposeront des bugs auxquels vous ne pourrez pas répondre.

Tâche 4 : Confirmer que le panneau n’est pas focalisable quand « fermé » (audit DOM)

cr0x@server:~$ node -e "const {JSDOM}=require('jsdom'); const html=require('fs').readFileSync('dist/index.html','utf8'); const d=new JSDOM(html).window.document; console.log(d.querySelectorAll('.panel a').length);"
18

Signification : Des liens existent dans le panneau ; assurez-vous maintenant que votre runtime utilise hidden ou du rendu conditionnel quand il est fermé.
Décision : Si les panneaux sont toujours visibles dans l’ordre de tabulation quand ils sont fermés, implémentez un basculement hidden en JS pour le mobile/basculement explicite.

Tâche 5 : Linter les erreurs d’attribut ARIA (victoire cheap en CI)

cr0x@server:~$ npx eslint src/nav/**/*.tsx
src/nav/MegaMenu.tsx
  88:17  error  aria-controls value must match an element id  jsx-a11y/aria-props

✖ 1 problem (1 error, 0 warnings)

Signification : Un contrôle référence un id de panneau qui n’existe pas (ou change à chaque rendu).
Décision : Corrigez les IDs pour qu’ils soient stables et uniques. Ne livrez jamais un aria-controls qui pointe vers nulle part ; c’est une fausse promesse.

Tâche 6 : Lancer Lighthouse localement et lire les indications liées à la nav

cr0x@server:~$ npx lighthouse http://localhost:4173 --only-categories=accessibility,performance --output=text
Performance: 86
Accessibility: 94
Diagnostics:
  Avoid enormous network payloads (main-thread impact)
Accessibility audits:
  Buttons do not have an accessible name (1)

Signification : Un bouton n’a pas de nom accessible (souvent le bouton chevron de divulgation).
Décision : Ajoutez un nom accessible via texte, aria-label ou aria-labelledby. Ne livrez pas de boutons icônes sans étiquette.

Tâche 7 : Lancer axe contre la page (signal a11y plus précis)

cr0x@server:~$ npx @axe-core/cli http://localhost:4173 --tags wcag2a,wcag2aa
Running axe-core 4.x
Violations:
  1) aria-required-attr: Required ARIA attributes must be provided
     - .menu-toggle (aria-expanded missing)

Signification : Votre contrôle de divulgation manque d’état requis.
Décision : Ajoutez aria-expanded et mettez-le à jour au basculement. Ce n’est pas optionnel si vous avez une région rétractable.

Tâche 8 : Vérifier que le comportement de survol est limité aux appareils avec pointeur/hover

cr0x@server:~$ rg -n "@media\s*\\(hover:hover\\)" src/styles/nav.css
148:@media (hover:hover) and (pointer:fine){

Signification : Vous scopez explicitement les règles de survol.
Décision : Si les règles de survol sont globales, vous aurez des bizarreries tactiles. Enrobez-les, puis implémentez le clic-pour-basculer pour les petits écrans.

Tâche 9 : Attraper les changements de layout quand le menu s’ouvre (test CLS)

cr0x@server:~$ npx playwright test -g "mega menu does not shift layout"
Running 1 test using 1 worker
✓  1 [chromium] › nav.spec.ts:14:1 › mega menu does not shift layout (2.3s)

Signification : Votre test affirme que la hauteur de l’en-tête ne change pas et que le contenu ne saute pas quand les panneaux s’ouvrent.
Décision : Si cela échoue, préférez des panneaux en position absolue sur desktop (overlay) ou réservez explicitement de l’espace. Ne laissez pas la page se reflower au survol.

Tâche 10 : Inspecter les problèmes de contexte d’empilement avec les styles calculés (triage z-index)

cr0x@server:~$ node -e "console.log('Check in DevTools: does any ancestor have transform/filter/opacity < 1? If yes, you created a stacking context.')"
Check in DevTools: does any ancestor have transform/filter/opacity < 1? If yes, you created a stacking context.

Signification : Ceci est une tâche de rappel, pas une automatisation. Les contextes d’empilement se déboguent le plus facilement visuellement dans DevTools.
Décision : Si le panneau est derrière quelque chose, retirez le transform/filter de l’ancêtre ou déplacez le panneau dans un conteneur portal de plus haut niveau.

Tâche 11 : Confirmer que les actifs clés ne bloquent pas la première interaction (vérif TTI)

cr0x@server:~$ npx webpack-bundle-analyzer dist/stats.json
Webpack Bundle Analyzer is started at http://127.0.0.1:8888
Use Ctrl+C to close it

Signification : Vous pouvez visualiser si le code du méga menu a fait entrer un chunk de bibliothèque UI de la taille d’une petite lune.
Décision : Si le menu coûte trop de JS, refactorez : CSS pour l’interaction desktop, JS minimal pour les basculements, et différer l’analytics non critique dans l’en-tête.

Tâche 12 : Vérifier que les basculements répondent à Échap (test comportemental)

cr0x@server:~$ npx playwright test -g "escape closes open mega menu"
Running 1 test using 1 worker
✓  1 [chromium] › nav.spec.ts:41:1 › escape closes open mega menu (1.8s)

Signification : Échap ferme le panneau ouvert et restaure le focus correctement (votre test doit affirmer le focus).
Décision : Si cela échoue, implémentez la gestion keydown sur le bouton de divulgation/conteneur de panneau et assurez la restauration du focus.

Tâche 13 : Repérer les éléments focalisables manquants dans le panneau ouvert (audit ordre de tabulation)

cr0x@server:~$ npx playwright test -g "tab reaches first link in opened panel"
Running 1 test using 1 worker
✘  1 [chromium] › nav.spec.ts:62:1 › tab reaches first link in opened panel (2.0s)
  Error: expected "body" to match /a.panel-link/

Signification : Après ouverture, tab n’a pas atteint le panneau ; le focus est retombé sur le body ou un élément non menu.
Décision : Vérifiez si le panneau est en display:none au mauvais moment, ou si un focus trap global vole le focus.

Tâche 14 : Confirmer que CSS Grid est bien appliqué (pas écrasé)

cr0x@server:~$ rg -n "display:\s*grid" dist/assets/index-*.css | head -n 3
1304:.panel-inner{display:grid;grid-template-columns:1.4fr 1fr 1fr;gap:14px}

Signification : Les styles Grid existent dans le build.
Décision : Si Grid manque, vous pourriez livrer une feuille de style legacy, ou vos styles de composant sont scoppés incorrectement.

Tâche 15 : Vérifier l’absence de désactivation accidentelle de pointer-events

cr0x@server:~$ rg -n "pointer-events:\s*none" src/styles | head
src/styles/animations.css:22:.no-pointer{pointer-events:none}

Signification : Vous avez une classe qui désactive les pointer events ; elle peut être appliquée accidentellement aux panneaux ou overlays.
Décision : Assurez-vous qu’elle n’est pas utilisée sur les conteneurs de navigation. « Le menu ne clique pas » est souvent auto-infligé.

Mode d’intervention rapide

Quand quelqu’un signale « méga menu cassé » cinq minutes avant une release, vous n’avez pas le temps de philosopher. Il vous faut un chemin court vers le goulot.
Voici l’ordre qui trouve la cause racine le plus vite dans les systèmes réels.

Premier : décalage du mode d’entrée (survol vs tap vs clavier)

  • Vérifiez sur un téléphone : le tap ouvre-t-il de manière fiable, et reste-t-il ouvert pendant le défilement ?
  • Vérifiez au clavier : tab vers le déclencheur, tab dans le panneau ; reste-t-il ouvert ?
  • Vérifiez trackpad/souris : un mouvement diagonal provoque-t-il du scintillement ?

Si un mode échoue, vous avez probablement couplé le comportement au survol, ou vous n’avez pas implémenté :focus-within, ou vous manquez d’état explicite pour le mobile.

Second : visibilité et contexte d’empilement (la classe « il est là mais je ne le vois pas »)

  • Dans DevTools, inspectez l’élément panneau. Est-il dans le DOM ? Est-il en display:none ?
  • Vérifiez le z-index calculé et si un ancêtre crée un contexte d’empilement (transform, filter).
  • Cherchez un overflow:hidden sur les wrappers d’en-tête qui coupent le panneau.

Si le panneau existe mais est derrière ou coupé, ne poussez pas les z-index. Corrigez le contexte d’empilement ou déplacez le panneau.

Troisième : dérive du focus et des états ARIA

  • aria-expanded reflète-t-il l’état visible ?
  • Le panneau est‑il réellement caché (avec hidden) quand il est replié ?
  • Un focus trap global empêche-t-il la tabulation dans le panneau ?

Si l’état ARIA ment, les utilisateurs de lecteurs d’écran rapporteront un comportement « aléatoire ». Ce n’est pas aléatoire ; vous diffusez un état incorrect.

Quatrième : instabilité induite par la performance

  • L’ouverture du menu est-elle saccadée à cause d’ombres lourdes, de filtres blur, ou trop d’images ?
  • Déclenchez-vous un thrash layout en animant height ou en mesurant le DOM en boucle ?

Si l’ouverture est lente, réduisez le coût de peinture et évitez les layouts forcés synchrones. Un méga menu doit s’ouvrir comme s’il avait honte de prendre votre temps.

Erreurs courantes : symptôme → cause → correctif

Ce sont les bugs qui réapparaissent parce que les équipes copient des patrons sans comprendre le problème résolu.
Utilisez cette section comme carte de diagnostic.

1) Le menu se ferme quand on déplace le pointeur vers le panneau

Symptôme : Le survol ouvre, mais le panneau disparaît quand vous essayez d’y entrer.

Cause : Il y a un espace entre le déclencheur et le panneau ; l’état hover est perdu.

Correctif : Ajoutez un « pont » avec du padding ou un pseudo-élément (::before) sur le panneau ; positionnez le panneau bord à bord avec le déclencheur.

2) Les utilisateurs clavier n’atteignent pas les liens du panneau

Symptôme : Tab ouvre le panneau, mais dès que le focus bouge, le panneau se referme.

Cause : Seul :hover ouvre le panneau ; pas de support :focus-within.

Correctif : Ouvrez le panneau sur li:focus-within, pas seulement au survol. Assurez-vous que le panneau est descendant du conteneur focus-within.

3) Sur mobile, le tap ouvre puis referme immédiatement

Symptôme : Le tap bascule mais l’état se renverse aussitôt.

Cause : Le gestionnaire « clic hors » se déclenche car l’événement bubble ; ou vous utilisez un écouteur document click sans exclusions.

Correctif : Arrêtez de traiter tout comme « extérieur ». Vérifiez la containment de event.target ; utilisez pointerdown en capture prudemment ; ignorez le clic du déclencheur qui a ouvert le panneau.

4) Le menu apparaît derrière l’en-tête ou le hero

Symptôme : Le panneau est ouvert (dans le DOM) mais invisible ou partiellement caché.

Cause : Contexte d’empilement ou découpage : un ancêtre a transform ou overflow:hidden.

Correctif : Retirez la propriété problématique, ou rendez le panneau dans un portal à la racine du document. Ensuite posez une échelle de z-index raisonnable.

5) Le lecteur d’écran annonce « replié » quand il est ouvert

Symptôme : L’état visuel et l’état assistif divergent.

Cause : aria-expanded pas mis à jour, ou panneau affiché via CSS sans mise à jour ARIA.

Correctif : Si une action utilisateur bascule la visibilité, mettez à jour aria-expanded dans le même flux de code. Préférez l’état explicite pour les basculements explicites.

6) La tabulation atterrit sur des liens invisibles

Symptôme : Le focus disparaît ; l’utilisateur clavier est perdu.

Cause : Le panneau est visuellement caché via opacity/transform mais reste dans l’ordre de tabulation.

Correctif : Utilisez hidden (ou rendu conditionnel) quand il est replié. Si vous devez animer, animez depuis hidden → visible avec une stratégie de transition courte qui ne le laisse pas focusable quand il est fermé.

7) La mise en page saute à l’ouverture du menu (CLS)

Symptôme : Le contenu se décale quand le panneau apparaît.

Cause : Le panneau est en flux normal (pas overlay) sur desktop ; l’ouverture change la hauteur de l’en-tête.

Correctif : Positionnez absolument le panneau sur desktop, ou réservez l’espace intentionnellement avec une région d’en-tête stable. Ne laissez pas le survol reflower la page.

8) Le méga menu devient un coût en performance

Symptôme : L’ouverture est saccadée ; FPS baisse ; batterie triste.

Cause : Flou/backdrop-filter lourds, trop d’ombres, images volumineuses, ou layout coûteux à l’ouverture.

Correctif : Réduisez les effets, pré-dimensionnez les images, évitez le blur. Si vous animez, animez opacity/transform. Mesurez sur du mobile milieu de gamme.

Listes de contrôle / plan étape par étape

Voici le plan que je remettrais à une équipe qui doit livrer un méga menu sans passer le trimestre suivant en triage de bugs.
C’est ennuyeux. C’est pourquoi ça marche.

Plan de construction étape par étape (dans cet ordre)

  1. Définir l’IA. Identifiez les catégories de top-level et les groupes de liens. Limitez le nombre d’items par groupe ; créez des pages « Tous… » pour le reste.
  2. Écrire le HTML sémantique en premier. Nav → ul/li → liens. Ajoutez des boutons de divulgation seulement là où un panneau existe.
  3. Implémenter les règles d’ouverture desktop en CSS. Utilisez :focus-within et le survol gate par capacité de pointeur.
  4. Implémenter la mise en page du panneau avec Grid. Construisez les groupes comme éléments de grille pour que les titres ne se séparent pas de leurs liens.
  5. Ajouter le JS minimal pour les basculements explicites. Gérez aria-expanded, hidden, Échap, et les clics hors (si overlay).
  6. Décision layout mobile. Accordéon en ligne ou tiroir. Ne tentez pas de préserver l’UX hover desktop.
  7. Règles de gestion du focus. Pour les tiroirs, déplacez le focus dedans et dehors ; assurez-vous que l’arrière-plan est inert quand approprié.
  8. Vérifications d’accessibilité en CI. Axe ou règles eslint qui bloquent les merges sur les régressions évidentes.
  9. Tests performance rapides. Évitez les filtres blur, réduisez le coût de peinture, surveillez la dérive JS.
  10. Tests multi-entrée. Clavier + souris + tactile. Testez aussi le zoom à 200% et l’augmentation de la taille du texte.

Checklist pré-merge (rapide mais stricte)

  • Le clavier peut atteindre chaque lien dans chaque panneau.
  • L’indicateur de focus est visible et cohérent.
  • aria-expanded se met à jour correctement sur les basculements.
  • Les panneaux ne sont pas focalisables quand ils sont fermés (hidden ou non rendus).
  • Sur tactile, le comportement des taps est déterministe (pas de bizarreries d’émulation hover).
  • Échap ferme l’état ouvert ; le focus revient au déclencheur.
  • Pas de saut de mise en page à l’ouverture sur les largeurs desktop.
  • Le panneau n’est pas coupé par overflow ; le z-index est sensé et documenté.

Checklist post-déploiement (parce que la prod est la vérité)

  • Surveillez les erreurs côté client autour du code de basculement de la nav.
  • Analysez les replays de session ou événements analytics pour des boucles d’ouverture/fermeture répétées (signe de mauvais taps ou de cibles de hit cassées).
  • Surveillez les métriques de performance (INP, CLS) après le déploiement.
  • Vérifiez que les bannières cookies, alertes et tests A/B n’ont pas superposé la nav de manière étrange.

Trois mini-récits d’entreprise

Mini-récit n°1 : L’incident causé par une fausse hypothèse

Une entreprise B2B a livré un en-tête redesigné avec un méga menu. Le designer l’a testé sur MacBook avec souris. L’ingénieur l’a testé en mode responsive de Chrome DevTools.
Les deux étaient confiants. La release est sortie un mardi parce qu’apparemment tout le monde voulait apprendre quelque chose.

En quelques heures, les tickets support ont montré un schéma : les utilisateurs mobiles ne pouvaient pas naviguer vers les sous-pages pricing. Le menu « s’ouvrait » puis se refermait avant qu’ils puissent taper quoi que ce soit.
Produit a supposé que c’était « un bug Safari ». L’ingénierie a supposé « problème de cible tap ». Marketing a supposé « les utilisateurs sont stupides ». Une seule de ces hypothèses était corrigeable.

La cause racine était une fausse hypothèse : « les règles hover n’importent pas sur mobile ». Ils avaient du CSS qui ouvrait les panneaux sur :hover et fermait au mouseout.
Sur iOS, le premier tap déclenchait un état de type hover, puis le gestionnaire de clic document considérait la seconde interaction comme un clic à l’extérieur et fermait le panneau.
Le composant se battait contre lui-même.

La correction fut peu glamour : restreindre le comportement hover derrière @media (hover:hover) and (pointer:fine), et utiliser un état de basculement explicite seulement sur mobile.
Ils ont aussi corrigé le handler de clic extérieur pour ignorer l’interaction d’ouverture.

Le postmortem fut encore plus ennuyeux : « Nous testerons sur un vrai téléphone avant de livrer des changements de navigation. » Cette politique a survécu parce qu’elle était facile à respecter
et a fait gagner du temps à tout le monde.

Mini-récit n°2 : L’optimisation qui a échoué

Une autre organisation avait un méga menu avec images et cartes promotionnelles dans le panneau. Quelqu’un a remarqué que l’ouverture du menu était lente sur vieux portables.
L’équipe a fait ce que les équipes font : ils ont optimisé. Ils ont décidé de lazy-loader tout ce qui était dans le panneau uniquement à l’ouverture, y compris des groupes de liens fetched depuis un endpoint CMS.

Sur le papier, c’était propre : ne pas rendre ce qu’on ne voit pas. En pratique, le panneau a maintenant une dépendance réseau sur le chemin d’interaction initial.
La première ouverture prenait un temps notable ; parfois il s’ouvrait vide, puis se peuplait. Les utilisateurs clavier tabulaient dans le vide et se retrouvèrent coincés.

Puis est venue la défaillance subtile : l’endpoint CMS était parfois lent. Pas en panne, juste assez lent. Le menu « marchait », mais devenait imprévisible.
Les clients le décrivaient comme « la nav est instable ». Instable est une façon polie de dire « je ne fais pas confiance à votre produit ».

Ils ont reverté l’approche fetch-on-open et choisi un plan plus simple : livrer les groupes de liens de base dans le payload HTML/JSON initial, et lazy-loader seulement les images strictement décoratives.
Le panneau s’est ouvert instantanément à nouveau, et la performance perçue s’est améliorée plus que le métrique original.

Leçon : optimiser en ajoutant des dépendances runtime sur des chemins UI critiques, c’est comme mettre ses mots de passe en clair pour aller plus vite. Oui, c’est plus rapide. Non, vous ne pouvez pas le faire.

Mini-récit n°3 : La pratique ennuyeuse mais correcte qui a sauvé la mise

Un grand site d’entreprise menait des dizaines d’expériences. L’en-tête était « possédé » par une équipe plateforme, mais diverses équipes growth injectaient des bannières, promos sticky,
et parfois un widget de chat qui insistait pour vivre en haut‑droite comme une plante d’intérieur agressive.

L’équipe plateforme maintenait une échelle de z-index en CSS et la faisait respecter en revue de code. Ils avaient aussi une règle : « Tout ce qui overlay l’en-tête doit être rendu dans une
racine d’overlay dédiée, pas dans des sections de page aléatoires. » C’était le type de politique qui semble pédante jusqu’au jour où ça ne l’est pas.

Un vendredi, une expérience growth a ajouté une section hero avec une animation transform subtile. Cela a créé un contexte d’empilement. Sur beaucoup de pages, le panneau méga est apparu derrière
le hero, rendant la navigation cassée. Growth voulait « juste mettre un z-index à un million ».

L’équipe plateforme n’a pas négocié avec le chaos. Comme la racine d’overlay était déjà standard, le panneau méga vivait en dehors du hero transformé.
La correction a été une ligne dans le CSS de l’expérience pour retirer le transform inutile sur le conteneur hero. Pas de course aux z-index. Pas d’incident de week-end.

La pratique ennuyeuse — règles de superposition documentées et une racine d’overlay dédiée — les a sauvés d’une classe de bugs qui autrement ne meurt jamais complètement.

FAQ

1) Puis-je construire un méga menu avec uniquement du CSS ?

Sur desktop, pour beaucoup de cas oui : le survol et :focus-within couvrent beaucoup. Sur mobile, vous voulez toujours du JavaScript pour gérer les basculements de tap et l’état ARIA.
« CSS-only » est une belle démo, pas une exigence produit fiable.

2) L’élément de premier niveau doit-il être un lien ou un bouton ?

S’il navigue, c’est un lien. S’il bascule une visibilité, c’est un bouton. Si vous avez besoin des deux, séparez-les : le label lien navigue ; le bouton adjacent bascule.
Le comportement mixte sur un seul contrôle, c’est là où l’UX va finir audité.

3) Ai-je besoin de role="menu" pour l’accessibilité ?

Non. Pour la navigation de site, utilisez les éléments natifs et ARIA pour l’état de divulgation (aria-expanded). Ajouter les rôles de menu change le comportement attendu du clavier
(touches fléchées, rôles menuitem). À moins d’implémenter le patron complet, n’y allez pas.

4) Comment empêcher que le panneau soit coupé sous l’en-tête ?

Cherchez overflow:hidden sur les wrappers d’en-tête et les contextes d’empilement dus aux transforms. Si vous ne pouvez pas les enlever, rendez le panneau dans un conteneur de niveau supérieur
(portal/overlay root) et positionnez-le par rapport au déclencheur.

5) Quelle est la meilleure façon de fermer le menu en cliquant à l’extérieur ?

Faites‑le seulement pour les overlays/tiroirs. Pour les panneaux hover desktop, les règles focus/hover gèrent naturellement la fermeture. Si vous implémentez le clic extérieur,
vérifiez panel.contains(event.target) et trigger.contains(event.target) avant de fermer, et prenez garde à l’ordre des événements.

6) Combien de colonnes pour un panneau méga ?

Commencez par 2–3 colonnes sur desktop. Plus de colonnes augmente le coût de lecture et réduit la taille des titres. Utilisez Grid avec collapse responsive ; laissez le contenu se wrapper.
Et si vous avez besoin de 6 colonnes, vous avez probablement besoin d’un meilleur regroupement.

7) Pourquoi mon menu au survol « scintille » seulement en diagonale ?

Parce que le pointeur quitte le déclencheur avant d’entrer dans le panneau. Ajoutez un pont de survol (::before), réduisez l’espace, ou agrandissez la zone du déclencheur.
Les timers peuvent masquer cela mais créent souvent de nouveaux problèmes.

8) Dois-je animer l’ouverture ?

Si vous pouvez ouvrir instantanément, faites-le. Si vous animez, gardez court, évitez les animations qui déclenchent le layout, et respectez la réduction de motion.
La navigation doit paraître réactive, pas théâtrale.

9) Comment tester cela de façon fiable en CI ?

Utilisez Playwright pour le comportement (ordre de tabulation, Échap ferme, tap mobile bascule). Utilisez axe pour les violations d’accessibilité.
Ajoutez une ou deux captures visuelles de régression pour le panneau ouvert à des points de rupture clés. Gardez les tests stables en évitant les hacks de temporisation.

10) inert suffit-il pour l’accessibilité d’un tiroir ?

C’est un outil puissant, mais vous devez toujours gérer le focus à l’ouverture/fermeture et assurer qu’Échap fonctionne.
Vérifiez aussi le support navigateur dans votre cible ; sinon utilisez une approche focus-trap bien testée.

Conclusion : prochaines étapes qui livrent vraiment

Un méga menu n’est pas un ornement design. C’est une infrastructure de production pour la navigation. Traitez-le comme un load balancer : comportement prévisible, valeurs par défaut sensées,
performance mesurable, et correction ennuyeuse.

Prochaines étapes :

  • Écrivez le HTML sémantique et décidez lien vs bouton par élément de premier niveau.
  • Implémentez le comportement desktop avec :focus-within et le survol restreint par capacité de pointeur.
  • Choisissez un modèle mobile (accordéon en ligne ou tiroir) et implémentez un état de basculement explicite avec aria-expanded correct.
  • Ajoutez au moins trois tests Playwright : flux de tab clavier, Échap ferme, et basculement tap mobile.
  • Lancez Lighthouse et axe, et connectez-en un au CI pour ne pas régresser un vendredi.

Ensuite faites le geste le plus sous-estimé en ingénierie : testez sur un vrai téléphone. La nav fonctionnera ou pas. La réalité se moque de votre bibliothèque de composants.

Construit avec l’hypothèse que la production trouvera vos cas limites. Parce que ce sera le cas.

← Précédent
Conceptions de refroidissement : 2 ventilateurs vs 3 ventilateurs — Ce qui change vraiment
Suivant →
Verra-t-on des PC grand public hybrides x86+ARM ?

Laisser un commentaire