Le bouton-menu
Un menu ARIA est fait pour des actions d'application, pas pour de la navigation. Poser role=menu sur un menu de navigation retire les liens de la liste des liens du lecteur d'écran. Démo, code, critères RGAA.
4 critères RGAA en jeu
Essayez-les
Navigation — des liens
Actions — des commandes
Deux menus, deux clavier différents, et c’est tout le sujet. Ouvrez « Ressources » : Tab passe d’un lien au suivant, comme partout ailleurs. Ouvrez « Actions sur l’audit » : Tab sort du menu, ce sont les flèches qui déplacent — et une lettre saute à l’entrée qui commence par elle. Dans les deux cas, Échap referme et rend le focus au bouton.
Cette démo existe aussi en page autonome.
La distinction qui décide de tout
role="menu" ne veut pas dire « menu » au sens du web. Il vient des interfaces
d’application : une barre de menus, des commandes, un clavier à part. Heydon
Pickering le résume dans Inclusive Components — ce motif est fait pour des actions, pas pour de la navigation.
Le RGAA appelle « menu de navigation » tout autre chose. Son glossaire le définit ainsi :
Liste de liens permettant une navigation spécifique dans le site, dans une rubrique ou dans une collection de pages.
Une liste de liens. Le composant qui l’ouvre est donc un bouton de dépliage — le même que celui de l’accordéon — et rien de plus.
La question à se poser tient en une phrase : est-ce que l’entrée mène ailleurs, ou est-ce qu’elle fait quelque chose ici ? « Tarifs », « Contact », « Mon compte » mènent ailleurs : ce sont des liens. « Dupliquer », « Exporter », « Supprimer » font quelque chose : ce sont des commandes, et le motif ARIA s’applique.
L’erreur courante
Poser role="menu" sur un menu de navigation, parce que le mot correspond.
<!-- À ne pas faire : ce sont des liens, pas des commandes -->
<button aria-haspopup="menu" aria-expanded="false">Ressources</button>
<ul role="menu">
<li role="none"><a role="menuitem" href="/accordeon">L'accordéon</a></li>
<li role="none"><a role="menuitem" href="/onglets">Les onglets</a></li>
</ul> Le code paraît plus savant que la version sans rôles. Il est moins utilisable, et pour trois raisons cumulées.
Les liens cessent d’être des liens. role="menuitem" remplace le rôle
natif du <a> : le lien disparaît de la liste des liens de la page, celle que
NVDA ouvre par Inser + F7 et que VoiceOver donne au
rotor. C’est un des premiers gestes d’un utilisateur de lecteur d’écran pour
parcourir un site ; votre navigation n’y figure plus.
Le clavier change sans prévenir. NVDA et JAWS basculent en mode formulaire
dans un role="menu" : les touches de navigation rapide — H pour
les titres, K pour les liens — ne fonctionnent plus, elles sont
rendues au composant. L’utilisateur doit deviner qu’il vient d’entrer dans un
widget qui a ses propres règles.
Le contrat n’est pas tenu. Annoncer « menu » promet les flèches,
Échap, et un seul arrêt de tabulation. L'APG y ajoute deux commodités optionnelles — Début/Fin quand les flèches ne bouclent pas, la recherche à l'initiale — que notre démo implémente. Ce qui est promis, un menu de navigation qui pose ces rôles ne le fournit presque jamais : le code ARIA est copié, le clavier ne suit pas.Trois variantes secondaires, qui coûtent chacune quelque chose :
- Le menu qui s’ouvre au survol seul. Il n’existe ni au clavier ni au toucher : personne d’autre que la souris ne l’atteint.
- Le menu fermé par
opacity: 0ouleft: -9999px. Ses liens restent focalisables : le focus quitte l’écran, se promène dans l’invisible, puis réapparaît plus bas. aria-expandedposé sur la liste au lieu du bouton. L’état existe, mais sur un élément que personne ne visite : le bouton reste muet.
Le code, pas à pas
1. Le menu de navigation : un bouton, une liste de liens
<button type="button" aria-expanded="false" aria-controls="menu-ressources">
Ressources
</button>
<ul id="menu-ressources" hidden>
<li><a href="/composants/accordeon">L'accordéon</a></li>
<li><a href="/composants/onglets">Les onglets</a></li>
</ul> Aucun rôle ARIA. Le <button> donne le focus, Entrée,
Ce que ce code conserve, et que la version avec role="menu" perd : les liens
restent des liens, la liste reste une liste (« liste de 2 éléments »), et le
clavier reste celui de la page.
2. Le menu d’actions : les trois rôles
<button
type="button"
aria-haspopup="true"
aria-expanded="false"
aria-controls="menu-actions"
>
Actions sur l'audit
</button>
<ul role="menu" id="menu-actions" aria-label="Actions sur l'audit" hidden>
<li role="none">
<button type="button" role="menuitem" tabindex="0">Dupliquer l'audit</button>
</li>
<li role="none">
<button type="button" role="menuitem" tabindex="-1">Exporter en HTML</button>
</li>
</ul> aria-haspopup annonce que le bouton ouvre un menu, avant même qu’on l’active. role="menu" déclare le groupe, role="menuitem" chaque commande.
Le role="none" sur les <li> n’est pas décoratif : dans un menu, un li n’est plus un élément de liste, et le laisser tel quel produit une structure
que les technologies d’assistance interprètent mal. On neutralise donc le
conteneur pour ne garder que les entrées.
L’aria-label du menu compte aussi : sans lui, l’annonce est « menu, élément
de menu, 1 sur 3 » — un menu de quoi, on ne sait pas.
3. Un seul arrêt de tabulation
entrees.forEach((entree, rang) => {
entree.tabIndex = rang === actif ? 0 : -1;
}); C’est le tabindex roulant, le même que celui des onglets, et pour la même raison : un menu de huit commandes ne doit pas coûter huit tabulations à qui veut simplement passer au contenu suivant. On entre dans le menu par son bouton, on s’y déplace aux flèches, la tabulation en sort.
C’est aussi ce qui interdit d’employer ce motif pour de la navigation : dans une liste de liens, on veut que Tab circule.
4. Les flèches, et le retour du focus
menu.addEventListener('keydown', (evenement) => {
const cibles = {
ArrowDown: actif + 1,
ArrowUp: actif - 1,
Home: 0,
End: entrees.length - 1,
};
if (evenement.key === 'Escape') {
fermer();
bouton.focus(); // sans quoi la tabulation repart du début de la page
return;
}
const cible = cibles[evenement.key];
if (cible === undefined) return;
evenement.preventDefault();
allerA((cible + entrees.length) % entrees.length);
}); Le preventDefault() évite que la flèche déplace le focus et fasse défiler
la page. Le modulo donne le bouclage, attendu de la dernière entrée vers la
première.
Le bouton.focus() est la ligne qu’on oublie. Quand le menu se ferme, l’élément
qui portait le focus n’existe plus : le navigateur le rend au corps du document,
et la tabulation suivante repart du haut de la page. Le focus revient donc au
bouton — à la fermeture par Échap comme après l’activation d’une
commande.
5. La recherche à l’initiale, et le raccourci à une touche
L’APG propose, en option, la recherche à l’initiale dans un menu ARIA : taper « e » saute à « Exporter ». C’est un raccourci clavier à une seule touche, et le critère 12.10 les encadre. Il reste acceptable ici pour une raison précise : le référentiel admet le raccourci « dans le cas d’un composant d’interface utilisateur » quand il « ne peut être activé que si le focus clavier est sur ce composant ».
Autrement dit : l’écouteur va sur le menu ouvert, jamais sur document. Sur le
document, il capturerait le « e » de quelqu’un qui dicte, ou qui navigue à la
lettre — et il faudrait alors retomber sur l’une des deux autres portes du
12.10 : un mécanisme pour désactiver le raccourci, ou un mécanisme pour le
reconfigurer avec une touche de modification.
6. Faut-il un menu, seulement ?
Trois questions avant d’écrire tout ce JavaScript :
- Les entrées sont-elles peu nombreuses ? Cinq liens tiennent dans une barre de navigation visible. Un menu ajoute un clic, un état et du code pour cacher ce qui pouvait rester affiché.
- Est-ce un choix dans un formulaire ? Alors c’est un
<select>, pas un menu : le composant natif est meilleur que tout ce qu’on écrira. - Est-ce un menu à plusieurs niveaux ? Les sous-menus doublent le code et les défauts. Une liste de liens dépliée, structurée par des titres, se parcourt mieux : les titres donnent un plan navigable au lecteur d’écran, ce qu’un sous-menu ne donne pas.
Au clavier
Le menu de navigation, où l’on ne fait que déplier une liste :
| Touche | Effet |
|---|---|
| Tab | Va au bouton, puis, menu ouvert, d’un lien au suivant. |
| Entrée / Espace | Sur le bouton : ouvre ou ferme la liste. |
| Échap | Ferme la liste et ramène le focus sur le bouton. |
Le menu d’actions, qui suit le motif ARIA :
| Touche | Effet |
|---|---|
| Entrée / Espace | Sur le bouton : ouvre le menu, focus sur la première entrée. |
| ↓ / ↑ | Sur le bouton : ouvre sur la première ou la dernière entrée. Dans le menu : entrée suivante ou précédente, avec bouclage. |
| Début / Fin (optionnel) | Va à la première ou à la dernière entrée. |
| Une lettre (optionnel) | Saute à l’entrée suivante qui commence par cette lettre. |
| Échap | Ferme le menu et ramène le focus sur le bouton. |
| Tab | Sort du menu et le referme, sans y circuler. |
Ce que le lecteur d’écran restitue
Sur le bouton de navigation : le nom, la nature, l’état — « Ressources, bouton, réduit ». Ouvert, la liste est annoncée pour ce qu’elle est : « liste de 3 éléments, L’accordéon, lien ».
Sur le bouton d’actions, aria-haspopup s’ajoute : « Actions sur l’audit,
bouton, menu, réduit ». À l’ouverture : « Actions sur l’audit, menu,
Dupliquer l’audit, élément de menu, 1 sur 3 ».
Le diagnostic inverse tient en trois constats. Si vous entendez « élément de
menu » sur ce qui est un lien de navigation, role="menu" est posé là où il ne
faut pas — vérifiez la liste des liens de la page, ils n’y sont plus. Si vous
n’entendez ni « réduit » ni « développé », aria-expanded manque ou reste figé
sur le bouton. Si la tabulation s’arrête sur chaque commande du menu d’actions,
le tabindex roulant n’est pas posé.
La formulation varie d’un lecteur d’écran à l’autre ; les informations, non.
Checklist
- Trancher d’abord : des liens qui mènent ailleurs, ou des commandes qui agissent ici.
- Pour de la navigation : un
<button>,aria-expanded, une liste de liens, aucun rôle ARIA. - Pour des commandes :
aria-haspopup,role="menu"nommé,role="menuitem",role="none"sur les<li>. aria-expandedsur le bouton, jamais sur la liste.- Le menu fermé masqué par
hidden— jamais par une opacité ou un décalage. - Le focus revient au bouton à chaque fermeture, Échap comprise.
- La recherche à l’initiale écoutée sur le menu ouvert, jamais sur
document.
Les critères RGAA en jeu
- Critère 7.1 — Compatibilité des scripts
Aucun élément HTML ne fait un menu déroulant : sans aria-expanded sur le bouton, ni le fait qu'il ouvre quelque chose ni son état ne sont exposés aux technologies d'assistance.
- Critère 7.3 — Contrôle des scripts au clavier
Ouvrir le menu, parcourir ses entrées et le refermer doit se faire au clavier seul — ce qu'un menu qui s'ouvre au survol de la souris ne permet à personne.
- Critère 12.8 — Cohérence de l’ordre de tabulation
Deux fois plutôt qu'une : si le focus ne revient pas au bouton après fermeture, la tabulation reprend au début de la page ; et un menu replié par une opacité nulle ou un décalage hors écran garde ses liens dans l'ordre de tabulation, où le clavier se perd.
- Critère 12.10 — Raccourcis clavier à une seule touche
La recherche à l'initiale, proposée en option par l'APG dans un menu ARIA, est un raccourci à une seule touche : posée sur le document plutôt que sur le menu ouvert, elle capture les frappes de tout le monde.
Vérifiez votre composant avec l'extension
Pour aller plus loin
- L’accordéon : le même bouton de dépliage, appliqué à du contenu plutôt qu’à des liens.
- Les onglets : l’autre composant à tabindex roulant, où les flèches sont également de rigueur.
- Critère 12.10 — Raccourcis clavier à une seule touche : ce que la recherche à l’initiale doit respecter.
- « Menu et barre de navigation » au glossaire : la définition qui range un menu de navigation du côté des listes de liens.
Sources
- ARIA APG — Menu Button Pattern — le contrat de comportement d’un vrai menu
- ARIA APG — Disclosure Pattern — le motif à employer pour un menu de navigation
- MDN — ARIA
menurole — ce que le rôle change, et pour quels contenus - WCAG — Understanding 2.1.4 Character Key Shortcuts — pourquoi un raccourci à une touche doit être encadré
- Inclusive Components — Menus & Menu Buttons — la distinction actions/navigation, à la source