Construire une navigation en arborescence accessible
Dans ce tutoriel, vous allez construire une sidebar de navigation hierarchique (le genre de menu qu'on trouve a gauche d'une documentation) qui reste utilisable au clavier et au lecteur d'ecran, et qui fonctionne meme sans JavaScript.
On avance par etapes : d'abord un socle HTML solide, puis on l'enrichit. A la fin, vous saurez pourquoi ce pattern est plus robuste qu'un vrai widget d'arbre ARIA, et comment le tester.
Prerequis : des bases en HTML et un peu de JavaScript. Aucun framework n'est requis.
Avant de commencer : choisir le bon pattern
C'est la decision la plus importante, et on la prend tout de suite. Deux patterns ARIA semblent repondre au besoin, mais leur cout n'est pas le meme.
| Pattern | Quand l'utiliser | Cout |
|---|---|---|
| Liste imbriquee + disclosure (celui de ce tuto) | Des liens de navigation hierarchiques | Faible : HTML semantique + un bouton aria-expanded par section |
role="tree" (W3C Tree View) |
Un vrai widget de selection (explorateur de fichiers : un seul tabstop, navigation aux fleches) | Eleve : gerer les fleches, Home/End, la saisie rapide, aria-selected, le tabindex roving |
Pour une navigation (chaque item est un lien vers une page), le pattern tree
est surdimensionne et fragile : il capture les fleches et deroute l'utilisateur
de lecteur d'ecran, qui s'attend a de simples liens. On retient donc la liste
imbriquee avec disclosure : plus robuste, et naturellement compatible avec
l'amelioration progressive.
Regle a retenir : n'utilisez
role="tree"que si vous construisez un vrai widget de selection mono-tabstop. Une sidebar de liens n'en est pas un.
Etape 1 — Poser la structure HTML (sans JavaScript)
Le socle est une liste imbriquee de liens, rendue cote serveur. Sans une
seule ligne de JS, elle est deja navigable (Tab entre les liens) et semantiquement
correcte.
<nav aria-label="Documentation"> <ul> <li> <a href="/docs/guides">Guides</a> <ul> <li><a href="/docs/guides/demarrage">Demarrage</a></li> <li><a href="/docs/guides/configuration" aria-current="page">Configuration</a></li> </ul> </li> <li><a href="/docs/glossaire">Glossaire</a></li> </ul> </nav>
Ce qui compte ici :
<nav aria-label="...">cree un repere (landmark) nomme, distinct des autres<nav>de la page. Le lecteur d'ecran pourra sauter directement dessus.- Les listes imbriquees
<ul>/<li>portent la hierarchie par la semantique, pas par du CSS. Le lecteur d'ecran annonce « liste, N elements » et signale l'imbrication. aria-current="page"marque le lien de la page courante (on y revient a l'etape 4).
Testez des maintenant : la nav est deja pleinement utilisable au clavier.
Etape 2 — Ajouter le repli/deploiement (disclosure)
Le JavaScript n'apporte ici qu'un confort : replier les branches. Chaque section
repliable recoit un bouton portant aria-expanded, separe du lien. Un bouton
agit, un lien navigue : on ne melange jamais les deux roles.
<li> <button type="button" aria-expanded="true" aria-controls="sect-guides"> <span class="chevron" aria-hidden="true"></span> <span class="visually-hidden">Deplier/replier </span>Guides </button> <a href="/docs/guides">Guides</a> <ul id="sect-guides"> ... </ul> </li>
document.querySelectorAll('nav [aria-expanded]').forEach((btn) => { btn.addEventListener('click', () => { const open = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!open)); document.getElementById(btn.getAttribute('aria-controls')).hidden = open; }); });
Ce qui compte ici :
- L'etat est porte par
aria-expanded(annonce « reduit » / « developpe »), et non par une simple classe CSS. hiddenmasque la sous-liste pour tout le monde : visuel, lecteur d'ecran et tabulation. Pas d'element focusable cache qui pourrait pieger l'utilisateur.- Le chevron, purement decoratif, est neutralise par
aria-hidden="true".
Etape 3 — Verifier le clavier et le focus
Bonne nouvelle : avec le pattern disclosure, rien d'exotique. L'ordre de tabulation natif suffit.
Tab/Maj+Tabparcourt boutons et liens dans l'ordre du DOM.Entree/Espaceactive le bouton (replie/deplie) ou suit le lien.- Le focus visible est obligatoire (
:focus-visible) : ne faites jamaisoutline: nonesans un remplacement net et bien contraste. - Ne piegez pas le focus, et n'utilisez pas de
tabindexpositif.
Etape 4 — Marquer la page active
L'item courant porte aria-current="page", accompagne d'un style visuel qui ne
repose pas uniquement sur la couleur (ajoutez un fond, une bordure ou un poids de
police). Ses parents repliables sont deplies au chargement, pour que l'item actif
soit visible d'emblee.
Etape 5 — Generer l'arbre cote serveur
En pratique, l'arbre est genere plutot qu'ecrit a la main. Le principe : un rendu
recursif qui parcourt une structure de noeuds (chaque noeud a un libelle, une
cible et d'eventuels enfants) et produit l'imbrication <ul>/<li>.
Voici l'idee en pseudo-template (adaptez a votre moteur de rendu) :
fonction branche(noeuds, cheminCourant) :
ouvrir <ul>
pour chaque n dans noeuds :
ouvrir <li>
<a href="lien(n)" [aria-current="page" si n.cible == cheminCourant]>n.libelle</a>
si n.enfants : branche(n.enfants, cheminCourant)
fermer </li>
fermer </ul>
Le controleur fournit deux choses : la liste des noeuds (l'arbre construit depuis
votre source de contenu) et le cheminCourant (pour marquer la page active). Pensez
a appliquer vos exclusions — fichiers internes, brouillons, contenus non publies —
avant de construire l'arbre, pas dans le template.
Tester
- Sans JS : desactivez JavaScript. La nav doit rester entierement navigable (les liens sont servis par le serveur).
- Clavier seul :
Tabatteint chaque lien et chaque bouton, le focus est visible,Entree/Espacereplient et deplient. - Lecteur d'ecran (par ex. NVDA ou VoiceOver) : le repere « Documentation » apparait dans la liste des reperes ; l'etat « developpe / reduit » est annonce ; la page courante est identifiee.
- Automatique : un test fonctionnel qui verifie la presence de
nav[aria-label], l'unicite dearia-current="page", et la presence dearia-expandedsur les boutons de section.
Pour aller plus loin
- W3C ARIA APG — Disclosure (le pattern de ce tuto) et Tree View (pour le cas d'un vrai widget de selection).