Menu button accessible : role menu, souvent le mauvais choix
Le motif menu button ARIA sert un menu d'actions, pas la navigation d'un site : quand préférer nav ou un disclosure, clavier attendu, aria-haspopup et critères RGAA.
Avant toute chose : le motif menu button est le plus souvent le mauvais choix. role="menu" modélise un menu d'application de bureau, c'est-à-dire une liste d'actions à déclencher (Modifier, Dupliquer, Supprimer), pas le mot « menu » au sens du web design. Un bouton qui révèle la navigation du site ou un panneau de contenu est un disclosure (aria-expanded sur un bouton), sans aucun ARIA de menu. Le vrai menu button est un bouton qui ouvre un authentique menu d'actions, avec navigation aux flèches et déplacement du focus réel. Pour un menu déroulant de navigation, le pas-à-pas est du côté du blog : Menus déroulants accessibles : Guide technique complet.
Quand ce motif est le mauvais choix
- Le panneau contient des liens de navigation (Accueil, À propos, Contact) :
<nav>+<ul>+<a>, sans ARIA. Des liens ne sont pas des éléments de menu. - Un bouton « hamburger » qui révèle la navigation : un disclosure enveloppant un
<nav>. - Le panneau contient du contenu arbitraire (formulaire, texte, panneau riche) : disclosure encore.
- Le composant choisit une valeur soumise dans un formulaire : une combobox ou un
<select>. - Des options de filtre ou de tri qui modifient une vue : disclosure + cases à cocher ou boutons radio, qui sont des contrôles de formulaire.
- Le panneau contient des actions sur un objet (Modifier, Dupliquer, Supprimer) : menu button, cette fiche.
Le coût de l'erreur est concret : posez role="menu" sur une navigation et le lecteur d'écran annonce « menu, 5 éléments » puis bascule en mode application ; les touches de lecture habituelles cessent de fonctionner ; l'utilisateur est invité à naviguer aux flèches, que des liens n'implémentent pas. C'est strictement pire que le même balisage sans aucun ARIA. En cas de doute, le disclosure est presque toujours la bonne réponse, et bien plus difficile à rater.
Critères RGAA applicables
- Critère 7.1 : compatibilité des scripts avec les technologies d'assistance.
aria-haspopup="menu"etaria-expandedsur le bouton exposent ensemble « bouton menu, réduit » : l'utilisateur sait qu'il ouvre un menu avant même d'appuyer. - Critère 7.3 : script contrôlable au clavier et par tout dispositif de pointage. Entrée, Espace ou Flèche bas ouvrent le menu ; les flèches le parcourent ; Échap le ferme ; la souris permet les mêmes actions.
- Critère 10.7 : prise de focus visible. Le focus réel se déplace d'élément en élément dans le menu et doit rester visible à chaque étape.
- Critère 10.8 : contenus cachés ignorés des technologies d'assistance. Le menu fermé se masque avec
hidden, pas seulement en CSS visuel, pour n'être ni restitué ni tabulable. - Critère 12.8 : ordre de tabulation cohérent. À l'ouverture, le focus se place sur le premier élément ; à la fermeture par Échap, il revient au bouton.
- Critère 12.9 : absence de piège au clavier. Échap et Tab doivent toujours permettre de sortir du menu.
Interaction clavier attendue
| Touche | Action |
|---|---|
| Entrée / Espace / Flèche bas (sur le bouton) | Ouvre le menu et place le focus sur le premier élément |
| Flèche haut (sur le bouton) | Ouvre le menu et place le focus sur le dernier élément |
| Flèche bas / Flèche haut (dans le menu) | Élément suivant / précédent, avec bouclage |
| Début / Fin | Premier / dernier élément |
| Échap | Ferme le menu et rend le focus au bouton |
| Tab | Ferme le menu et poursuit la tabulation, jamais piégé, jamais renvoyé au bouton |
Rôles et attributs ARIA
Contrairement à la combobox, ce motif déplace le focus réel : on appelle .focus() sur l'élément de menu, jamais aria-activedescendant. Les seuls enfants valides de role="menu" sont menuitem, menuitemradio et menuitemcheckbox : un <a href> ou un <button> nu y est invalide et restitué de façon incohérente. Chaque élément porte tabindex="-1" (roving tabindex) : on l'atteint aux flèches, jamais à Tab.
<button type="button" id="btn-actions" aria-haspopup="menu"
aria-expanded="false" aria-controls="menu-actions">
Actions
</button>
<!-- Éléments = <button role="menuitem"> : le bouton natif fournit focus et
activation, le rôle menuitem remplace son rôle par celui que role="menu"
exige de ses enfants. tabindex="-1" les sort de l'ordre de tabulation. -->
<ul id="menu-actions" role="menu" aria-labelledby="btn-actions" hidden>
<li role="none">
<button type="button" role="menuitem" tabindex="-1">Modifier</button>
</li>
<li role="none">
<button type="button" role="menuitem" tabindex="-1">Dupliquer</button>
</li>
<li role="none">
<button type="button" role="menuitem" tabindex="-1">Supprimer</button>
</li>
</ul>
role="none" sur les <li> retire leur sémantique d'élément de liste, qui s'intercalerait sinon entre le menu et ses éléments. Et aria-expanded vit sur le bouton, jamais sur le menu.
function ouvrirMenu(indice = 0) {
bouton.setAttribute('aria-expanded', 'true');
menu.hidden = false;
elements[indice].focus(); // focus RÉEL, pas aria-activedescendant
}
function fermerMenu({ rendreFocus = true } = {}) {
bouton.setAttribute('aria-expanded', 'false');
menu.hidden = true;
if (rendreFocus) bouton.focus(); // retour au bouton : Échap oui, Tab non
}
menu.addEventListener('keydown', (evenement) => {
const indice = elements.indexOf(document.activeElement);
if (indice === -1) return;
if (evenement.key === 'ArrowDown') { evenement.preventDefault(); elements[(indice + 1) % elements.length].focus(); }
if (evenement.key === 'ArrowUp') { evenement.preventDefault(); elements[(indice - 1 + elements.length) % elements.length].focus(); }
if (evenement.key === 'Escape') fermerMenu();
if (evenement.key === 'Tab') fermerMenu({ rendreFocus: false }); // Tab passe (pas de preventDefault)
});
// Fermeture au clic extérieur : pointerdown, pas click, pour ne pas avaler
// le clic sur un élément du menu.
document.addEventListener('pointerdown', (evenement) => {
if (!menu.hidden && !evenement.target.closest('.bouton-menu')) {
fermerMenu({ rendreFocus: false });
}
});
À ne pas faire :
<!-- À ne pas faire : role="menu" sur la navigation du site.
L'erreur emblématique du motif. Le lecteur d'écran bascule en mode
application, les touches de lecture cessent de fonctionner et les
flèches promises ne font rien sur des liens. La navigation d'un site,
c'est <nav><ul><li><a>, sans rien d'autre. -->
<nav>
<ul role="menu">
<li role="none"><a role="menuitem" href="/">Accueil</a></li>
<li role="none"><a role="menuitem" href="/a-propos">À propos</a></li>
</ul>
</nav>
Défauts fréquents et impact utilisateur
role="menu"sur la navigation du site : mode application forcé, touches de lecture perdues, flèches inopérantes sur des liens. Pire que l'absence totale d'ARIA.- Liens
<a href>dans unrole="menu": enfants invalides, annoncés tantôt « lien », tantôt « élément de menu » selon le lecteur d'écran. Et si les éléments naviguent, ce n'est pas un menu. - Ouvrir le menu sans y déplacer le focus : les éléments étant en
tabindex="-1", l'utilisateur clavier voit un menu ouvert qu'il ne peut jamais atteindre. aria-expandedposé sur le menu au lieu du bouton : le bouton n'annonce aucun état ; l'utilisateur ne sait jamais qu'il ouvre quelque chose.- Fermer le menu au
blurdu bouton : le focus qui entre dans le menu fait perdre le focus au bouton, donc le menu se ferme à l'instant précis où il s'ouvre. - Renvoyer le focus au bouton quand Tab ferme le menu : Tab semble ne rien faire et l'utilisateur tourne en boucle. Le retour au bouton est réservé à Échap.
Ce que les outils automatiques ne détectent pas
Un scanner automatique, le nôtre compris, repère des enfants invalides dans un role="menu" ou un menu sans nom accessible. Mais le défaut principal lui échappe entièrement :
role="menu"posé sur une navigation est de l'ARIA parfaitement valide et parfaitement faux. Seul le jugement humain (les éléments naviguent-ils ?) le détecte.- L'arrivée du focus sur le premier élément à l'ouverture, et son retour sur le bouton à la fermeture par Échap, ne se vérifient qu'au clavier.
- L'annonce attendue (« Actions, bouton menu, réduit ») : si seul « bouton » est annoncé,
aria-haspopupmanque, et rien ne le signalera automatiquement. - La fermeture au clic extérieur et l'absence de piège à Tab restent des vérifications manuelles.
Vérifier ce composant
Protocole manuel, trois minutes :
- La question préalable, avant tout test technique : que contient le panneau ? Des éléments qui naviguent vers une autre page ne font pas un menu, quel que soit le soin du balisage. Le quiz Bouton ou Lien ? entraîne précisément ce réflexe action / navigation.
- Au clavier seul : Entrée ouvre le menu et le focus doit atterrir sur le premier élément (s'il reste sur le bouton, le menu est inatteignable). Les flèches bouclent. Échap ferme et rend le focus au bouton. Tab ferme et avance, sans jamais revenir en arrière.
- Au lecteur d'écran : le bouton doit annoncer « Actions, bouton menu, réduit », puis « développé » une fois ouvert.
Le simulateur de lecteur d'écran affiche l'arbre d'accessibilité : aria-haspopup, aria-expanded et les rôles menu / menuitem s'y lisent directement, avant même le test au vrai lecteur d'écran décrit dans le guide Tester avec un lecteur d'écran.
Approfondir avec les fiches critères
Vérifier ce composant sur votre site ?
Le scan repère les défauts détectables automatiquement (rôles incohérents, champs sans étiquette, attributs ARIA orphelins) ; le reste se vérifie à la main avec les protocoles de cette fiche.
Lancer un scan gratuit