tutorial/treeview-accessible.md

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.
  • hidden masque 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+Tab parcourt boutons et liens dans l'ordre du DOM.
  • Entree / Espace active le bouton (replie/deplie) ou suit le lien.
  • Le focus visible est obligatoire (:focus-visible) : ne faites jamais outline: none sans un remplacement net et bien contraste.
  • Ne piegez pas le focus, et n'utilisez pas de tabindex positif.

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 : Tab atteint chaque lien et chaque bouton, le focus est visible, Entree/Espace replient 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 de aria-current="page", et la presence de aria-expanded sur 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).

Assistant documentaire

Posez une question sur la documentation. Les réponses citent leurs sources — un clic ouvre le document à gauche.