RGAA Lab

Composants accessibles

Le menu de navigation

Un menu de navigation, ce sont trois pièces : un nav qui porte un nom, une vraie liste de liens, une marque sur la page affichée. Et le bouton qui l'ouvre est un bouton, pas une case à cocher détournée. Démo, code, critères RGAA.

6 critères RGAA en jeu

Essayez-le

Tabulez : le focus passe par le bouton, puis par chaque lien du menu ouvert. Refermez le menu, tabulez encore — plus aucun arrêt, parce que le menu est masqué et non seulement replié. Puis relevez les repères : les deux nav portent un nom distinct, et un seul lien est marqué comme la page affichée.

Cette démo existe aussi en page autonome.

L’erreur courante

Deux nav sans nom, des liens qui ne forment pas une liste, et un état d’ouverture qui n’existe que dans une classe CSS.

<!-- À ne pas faire -->
<div class="burger" onclick="basculer()"></div>

<nav class="menu">
	<a href="/">Accueil</a>
	<a href="/produits">Produits</a>
	<a href="/contact" class="actif">Contact</a>
</nav><nav>
	<a href="/rubrique/tarifs">Tarifs</a>
	<a href="/rubrique/options">Options</a>
</nav>
/* À ne pas faire : le CSS sait que le menu est ouvert, personne d'autre. */
.menu {
	left: -9999px;
}
.menu.ouvert {
	left: 0;
}

Ce que cela coûte, dans l’ordre où on s’en aperçoit.

Un lecteur d’écran dresse la liste des repères de la page pour y sauter directement. Ici, il annonce « navigation, navigation » : deux entrées identiques, rien pour choisir. C’est un raccourci de navigation rendu inutile par un attribut manquant.

Les liens ne sont pas dans une liste : ni le nombre d’entrées, ni le rang de celle qu’on lit. On avance sans savoir s’il en reste deux ou douze.

Le div cliquable ne prend pas le focus, ne répond ni à Entrée ni à

Espace, et n'annonce ni sa nature ni son état. Et surtout, le menu « fermé » est toujours là : sorti de l'écran, il garde ses liens focalisables. Le focus disparaît, parcourt un menu invisible, puis revient. C'est le défaut le plus déroutant qui soit au clavier, et il est indétectable à la souris.

Trois variantes, aussi répandues :

  • La case à cocher détournée — un <input type="checkbox"> masqué, un <label> en guise de bouton, et :checked ~ .menu pour ouvrir. Le clavier fonctionne, mais l’annonce est fausse : « case à cocher, non cochée » là où il faudrait « bouton, réduit ». L’utilisateur cherche un formulaire.
  • role="menu" sur une navigation de site — ces rôles décrivent le menu d’une application, avec navigation aux flèches et un seul arrêt de tabulation. Posés sur des liens ordinaires, ils promettent un comportement que le composant ne tient pas.
  • La page courante distinguée par la seule couleur — invisible pour qui ne la perçoit pas, inaudible pour qui écoute.

Le code, pas à pas

1. Un <nav>, et un nom dès qu’il y en a deux

<nav aria-label="Principale"></nav>

<nav aria-label="Dans cette rubrique"></nav>

<nav> est un repère : un lecteur d’écran en dresse la liste et permet d’y sauter sans traverser la page. Un seul nav n’a pas besoin de nom — « navigation » le désigne sans ambiguïté. À partir de deux, le nom est la seule chose qui les distingue.

Le nom ne répète pas le rôle. Écrivez aria-label="Principale", pas aria-label="Navigation principale" : le rôle est déjà annoncé, et « navigation Navigation principale » n’ajoute rien.

Le critère 9.2 va dans les deux sens : les zones de navigation se structurent par un nav, et le nav leur est réservé. Une grille de cartes, une liste de partenaires, une barre d’actions ne sont pas des zones de navigation ; leur poser un nav ajoute un repère qui égare au lieu d’aider.

2. Une vraie liste de liens

<nav aria-label="Principale">
	<ul>
		<li><a href="/">Accueil</a></li>
		<li><a href="/produits">Produits</a></li>
		<li><a href="/contact">Contact</a></li>
	</ul>
</nav>

Le glossaire du RGAA définit le menu de navigation comme une « liste de liens » : ce n’est pas une préférence de mise en forme. Ce que la liste apporte tient en deux mots — le nombre et le rang : « liste de 3 éléments, Produits, lien, 2 sur 3 ». Sans elle, des liens qui se suivent, et rien pour savoir où l’on en est.

La disposition horizontale ne change rien : c’est du CSS posé sur une liste, pas autre chose qu’une liste.

Si les puces sont retirées (list-style: none), ajoutez role="list" — Safari retire alors le rôle de liste, et VoiceOver n’annonce plus rien. Ce n’est pas une non-conformité au critère 9.3, c’est une précaution ; la fiche « listes » détaille pourquoi.

3. aria-current sur la page affichée

<li><a href="/produits" aria-current="page">Produits</a></li>

aria-current="page" désigne le lien qui mène à la page où l’on se trouve, et lui seul : un par page. Aucun test du RGAA ne l’exige nommément, mais le glossaire du critère 3.1 le range parmi les compléments au niveau du code attendus dès que la page affichée est distinguée à l’œil — c’est l’objet du paragraphe suivant.

Deux précautions valent d’être prises ensemble.

[aria-current='page'] {
	font-weight: 700;
	box-shadow: inset 3px 0 0 0 var(--accent);
}

Le style suit l’attribut, pas une classe : l’état vu et l’état exposé viennent alors de la même source et ne peuvent pas diverger. Le défaut classique est une classe .actif tenue à jour et un aria-current oublié en chemin.

Et la distinction visuelle ne repose pas sur la seule couleur. Le glossaire « Information (donnée par la couleur) », rattaché au critère 3.1, demande les deux ensemble : « Un autre style typographique (gras, italique, taille de texte, autre police, etc) et par le biais d’un complément au niveau du code (aria-label, title, texte masqué, aria-current, etc.) ». La graisse et le trait pour l’œil, aria-current="page" pour les technologies d’assistance.

4. Le bouton d’ouverture, et son état

<button type="button" aria-expanded="false" aria-controls="menu-principal">
	Menu
</button>

<nav id="menu-principal" aria-label="Principale" hidden></nav>

Le <button> donne le focus, Entrée, Espace et l’annonce « bouton » — quatre comportements qu’on ne réécrit pas, et que ni un div ni un <label> de case à cocher ne rendent.

aria-expanded porte l’état, et c’est son seul domicile. Les trois barres qui se changent en croix disent la même chose à l’œil : doublon utile, à condition d’être masquées par aria-hidden="true" pour ne pas être annoncées en plus.

Le nom du bouton, lui, ne bouge pas : « Menu », ouvert comme fermé. Un intitulé qui bascule entre « Ouvrir le menu » et « Fermer le menu » se défend, mais il ne dispense pas d’aria-expanded — l’intention et l’état sont deux informations différentes.

Enfin, si le menu est déployé en permanence au-delà d’un point de rupture, le bouton doit alors disparaître vraiment, par display: none. Réduit à zéro pixel ou rendu transparent, il reste focalisable, et la tabulation s’arrête sur un bouton que personne ne voit.

5. Le menu refermé : masqué, pas déplacé

<nav id="menu-principal" aria-label="Principale" hidden></nav>

hidden retire le menu de l’affichage et de l’ordre de tabulation, en un attribut. Un déplacement hors écran, une hauteur nulle ou une opacité à zéro ne font que la moitié du travail : le contenu reste restituable et focalisable, alors qu’il a vocation à être ignoré tant qu’il est fermé.

Attention à une règle CSS qui poserait un display sur l’élément masqué : elle l’emporte sur le display: none que le navigateur applique à [hidden], et le menu reste là sans que rien ne le signale. Un nav mis en display: flex suffit à annuler l’attribut.

6. Les sous-menus déroulants — en avez-vous besoin ?

Un sous-menu qui ne s’ouvre qu’au survol de la souris n’existe pas au clavier : c’est le défaut le plus courant des menus à niveaux, et le critère 12.11 le vise directement. Ce qui s’ouvre au survol doit s’ouvrir aussi à la prise de focus ou à l’activation, et rester ouvert le temps qu’on le parcourt.

Le motif sûr est celui du point 4, répété : chaque entrée à sous-menu est un <button aria-expanded> suivi de sa propre liste. C’est le Disclosure Navigation Menu de l’ARIA APG, et il n’emploie aucun role="menu".

Mais posez d’abord la question : un menu principal court, doublé d’un sommaire de rubrique sur chaque page, remplace la plupart des menus déroulants. Il ne demande aucun script, se parcourt sans viser à la souris, et s’imprime.

Au clavier

ToucheEffet
TabAmène au bouton, puis à chaque lien du menu ouvert. Le menu refermé n’ajoute aucun arrêt.
Entrée / EspaceSur le bouton : ouvre ou referme le menu. Le focus reste sur le bouton.
EntréeSur un lien : suit le lien.
ÉchapReferme le menu et ramène le focus au bouton qui l’a ouvert.
Navigation par repèresSaute d’un nav à l’autre — c’est leur nom qui permet de choisir.

Les flèches ne servent pas ici. Elles appartiennent à role="menubar", c’est-à-dire au menu d’une application, pas à une navigation de site faite de liens.

Ce que le lecteur d’écran restitue

Sur le bouton : « Menu, bouton, réduit », puis « développé » après activation. En entrant dans le menu : « Principale, navigation, liste de 4 éléments, Accueil, lien, 1 sur 4 ». Sur le lien de la page affichée : « Produits, lien, page courante, 2 sur 4 ».

La formulation varie d’un lecteur à l’autre ; les informations, non — le nom du repère, le nombre d’entrées, le rang, la page courante.

Le diagnostic inverse est plus utile encore. Si la liste des repères annonce deux fois « navigation » sans les distinguer, un aria-label manque. Si vous n’entendez pas « 1 sur 4 », les liens ne sont pas dans une liste — ou la liste a perdu son rôle en perdant ses puces. Si le bouton reste « réduit » une fois le menu ouvert, aria-expanded n’est pas mis à jour et seule la classe CSS bouge.

Checklist

  • Un <nav> par zone de navigation, et rien d’autre dans un <nav>.
  • Un aria-label sur chacun dès qu’il y en a deux, sans y répéter « navigation ».
  • Les liens dans une <ul>, avec role="list" si les puces sont retirées.
  • aria-current="page" sur un seul lien, jamais distingué par la seule couleur.
  • Un <button> avec aria-expanded tenu à jour — jamais une case à cocher.
  • Le menu refermé masqué par hidden, pas déplacé hors de l’écran.
  • Les sous-menus atteignables au clavier, pas seulement au survol.

Les critères RGAA en jeu

  • Critère 9.2 — Cohérence de la structure du document

    Une zone de navigation se structure par une balise nav, et la balise nav lui est réservée : un menu bricolé en div ne devient pas un repère, et un nav posé sur une grille de cartes en annonce un qui n'existe pas.

  • Critère 9.3 — Structure des listes

    Le glossaire définit le menu comme une liste de liens. Sans ul et li, plus de nombre ni de rang : on entend des liens sans savoir combien il en reste, et retirer les puces peut suffire à perdre ce rôle.

  • Critère 7.1 — Compatibilité des scripts

    Le bouton qui déploie le menu est un composant d'interface piloté par script : son nom, son rôle et son changement d'état doivent être exposés, ce qu'aria-expanded fait et qu'une classe CSS ne fait pas.

  • Critère 12.8 — Cohérence de l’ordre de tabulation

    Un menu refermé par un déplacement hors écran ou une hauteur nulle garde ses liens dans l'ordre de tabulation : le focus quitte l'écran, traverse des liens invisibles, puis réapparaît plus bas. Le masquer pour de bon — hidden — est ce qui l'en retire.

  • Critère 12.11 — Contenus additionnels au clavier

    Un sous-menu qui ne s'ouvre qu'au survol de la souris n'existe pas au clavier, alors qu'il contient souvent la moitié des rubriques du site.

  • Critère 12.2 — Place constante du menu

    Le menu doit garder la même place et le même rang dans le code source d'une page à l'autre : un gabarit intérieur qui injecte sa barre après le contenu détruit le repère que l'utilisateur s'est construit. La page d'accueil est exclue du critère.

Vérifiez votre composant avec l'extension

Pour aller plus loin

Sources