Composant · Menus et navigation

Arborescence accessible (treeview) : role tree, RGAA et clavier

Quand utiliser role=tree et quand l'éviter : une sidebar de navigation n'est pas un treeview. Critères RGAA, modèle clavier complet, aria-expanded et roving tabindex.

Commençons par la mise en garde, car c'est le défaut le plus répandu de ce motif : une barre latérale de navigation n'est pas un treeview. role="tree" bascule les lecteurs d'écran en mode application : les touches de lecture habituelles cessent de fonctionner et l'utilisateur doit connaître le modèle clavier propre aux arborescences. Ce coût est payé par chaque utilisateur, à chaque visite.

Une arborescence (tree view) est une liste hiérarchique dont les nœuds parents peuvent être dépliés ou repliés pour afficher ou masquer leurs enfants, en sélection simple ou multiple. Il n'existe aucun élément HTML natif équivalent, et c'est un indice : pour une navigation de site ou une liste de documents, des listes <ul> imbriquées avec des liens restent presque toujours préférables.

Quand ce motif est le mauvais choix

BesoinSolution correcte
Barre latérale de navigation avec sections dépliables<nav> + <ul> imbriquées + boutons de divulgation (aria-expanded)
Table des matières<nav> + <ul> imbriquées + <a>
Sélecteur de catégorielistbox ou <select>
Explorateur de fichiers : centaines de nœuds, déplier/replier, sélection pilotée au clavierarborescence role="tree"

Le test honnête : une application de bureau utiliserait-elle un widget d'arbre ici ? Un explorateur de fichiers, oui. Un menu de documentation, non.

Voici l'alternative qui convient dans la grande majorité des cas, sans mode application, sans gestion des flèches, sans apprentissage :

<!-- Listes imbriquées + boutons de divulgation. Chaque utilisateur sait
     déjà s'en servir : Tab de lien en lien, Entrée pour activer. -->
<nav aria-label="Documentation">
  <ul>
    <li>
      <button type="button" aria-expanded="true" aria-controls="liste-guides">Guides</button>
      <ul id="liste-guides">
        <li><a href="/guides/demarrage">Bien démarrer</a></li>
        <li><a href="/guides/formulaires">Formulaires</a></li>
      </ul>
    </li>
    <li><a href="/reference">Référence</a></li>
  </ul>
</nav>

Critères RGAA applicables

Interaction clavier attendue

C'est le modèle que les utilisateurs apportent du bureau (explorateur de fichiers). S'en écarter est pire que ne pas utiliser d'arborescence du tout.

ToucheAction
Tab / Maj + TabEntre dans l'arborescence (un seul arrêt de tabulation), puis en sort
Flèche bas / Flèche hautNœud visible suivant / précédent, sans jamais entrer dans les branches repliées
Flèche droiteDéplie un nœud fermé ; sur un nœud déjà ouvert, déplace le focus sur le premier enfant ; sans effet sur une feuille
Flèche gaucheReplie un nœud ouvert ; depuis un enfant, remonte au parent
Début / FinPremier / dernier nœud visible
EntréeActive le nœud focalisé
Saisie de caractèresType-ahead : déplace le focus sur le prochain nœud visible correspondant
* (optionnel)Déplie tous les nœuds frères du niveau courant
Espace (multi-sélection)Bascule la sélection du nœud focalisé ; Ctrl + A sélectionne ou désélectionne tout

Rôles et attributs ARIA

role="tree" sur le conteneur, nommé par aria-label ou aria-labelledby ; role="treeitem" sur chaque nœud ; role="group" sur le conteneur des enfants. Deux règles évitent la majorité des défauts : aria-expanded uniquement sur les nœuds qui ont des enfants, et de vraies listes imbriquées.

<span id="libelle-fichiers">Fichiers du projet</span>

<!-- Un seul arrêt de tabulation : exactement un nœud porte tabindex="0".
     L'imbrication réelle <ul> / role="group" laisse le navigateur calculer
     aria-level, aria-setsize et aria-posinset : ne les maintenez pas à la main. -->
<ul role="tree" aria-labelledby="libelle-fichiers">
  <li role="treeitem" aria-expanded="true" tabindex="0">
    <span class="arbre__libelle">src</span>
    <ul role="group">
      <li role="treeitem" tabindex="-1"><span class="arbre__libelle">index.js</span></li>
      <li role="treeitem" aria-expanded="false" tabindex="-1">
        <span class="arbre__libelle">composants</span>
        <!-- Branche repliée : masquée avec hidden, pas seulement en CSS
             (le contenu caché doit être ignoré des technologies d'assistance). -->
        <ul role="group" hidden>
          <li role="treeitem" tabindex="-1"><span class="arbre__libelle">Bouton.js</span></li>
        </ul>
      </li>
    </ul>
  </li>
  <!-- Une FEUILLE : pas d'aria-expanded, elle n'a rien à déplier. -->
  <li role="treeitem" tabindex="-1"><span class="arbre__libelle">LISEZMOI.md</span></li>
</ul>
/* Le focus réel se déplace de nœud en nœud : :focus-visible fonctionne
   et la prise de focus reste visible pendant la navigation aux flèches. */
[role="treeitem"]:focus-visible > .arbre__libelle {
  outline: 2px solid #0056b3;
  outline-offset: -2px;
}

/* Le pictogramme est piloté par l'attribut : l'état affiché et l'état
   annoncé ne peuvent pas diverger. */
[role="treeitem"][aria-expanded="true"] > .arbre__libelle::before { content: "▾ "; }
[role="treeitem"][aria-expanded="false"] > .arbre__libelle::before { content: "▸ "; }

Côté script, la règle structurante est de ne parcourir que les nœuds visibles :

// Uniquement les nœuds VISIBLES : un nœud contenu dans un groupe masqué
// n'est jamais navigable aux flèches.
const noeudsVisibles = () =>
  [...arbre.querySelectorAll('[role="treeitem"]')]
    .filter((noeud) => !noeud.closest('[role="group"][hidden]'));

En option : aria-multiselectable et aria-selected pour la sélection multiple ; aria-level, aria-setsize et aria-posinset uniquement si le DOM est aplati (une seule liste à plat). Avec de vraies listes imbriquées, le navigateur calcule ces valeurs seul, et les valeurs maintenues à la main se périment au premier réordonnancement.

Défauts fréquents et impact utilisateur

  • role="tree" sur une barre latérale de navigation : le mode application s'impose pour une simple liste de liens ; l'utilisateur de lecteur d'écran perd ses touches de lecture et doit apprendre le modèle des arborescences pour parcourir un menu.
  • aria-expanded="false" sur une feuille : le lecteur d'écran annonce « réduit » sur un élément qui n'a rien à déplier ; l'utilisateur presse Flèche droite et rien ne se passe.
  • tabindex="0" sur chaque nœud : une arborescence de 200 nœuds devient 200 arrêts de tabulation ; l'utilisateur clavier ne peut plus atteindre la suite de la page en un temps raisonnable.
  • Branche repliée masquée en CSS seul (height: 0; overflow: hidden) : les enfants restent exposés aux technologies d'assistance et le focus se déplace sur des nœuds invisibles à l'écran.
  • aria-level, aria-setsize et aria-posinset maintenus à la main sur un arbre réellement imbriqué : ces valeurs se périment au premier réordonnancement, et le lecteur d'écran annonce alors des niveaux et des positions faux, avec aplomb.
  • Contrôle interactif (bouton « Supprimer ») à l'intérieur d'un treeitem : les flèches naviguent entre les nœuds, jamais dedans ; le bouton est inatteignable. Si chaque nœud porte des actions, le motif adapté est la grille arborescente (treegrid), décrite dans la fiche grille (grid).

Ce que les outils automatiques ne détectent pas

Un scanner automatique, le nôtre compris, repère un role="treeitem" placé hors d'un tree et l'absence de nom accessible sur le conteneur. Mais les défauts qui comptent le plus ici lui échappent par nature :

  • le choix du motif lui-même : une arborescence utilisée pour de la navigation de site est le défaut le plus fréquent, et aucun outil automatique ne peut juger qu'un menu n'avait pas besoin d'être un arbre ;
  • aria-expanded posé sur des feuilles, et le focus qui atterrit dans une branche repliée : ces incohérences d'état exigent un test manuel au clavier et au lecteur d'écran ;
  • le modèle clavier complet (Flèche droite qui déplie puis descend, type-ahead, Tab qui sort) reste une vérification manuelle.

Vérifier ce composant

Protocole manuel, cinq minutes :

  1. La question préalable : ce composant devait-il être un arbre ? Si c'est de la navigation, la réponse est non, et c'est le premier constat à noter.
  2. Le test de la branche repliée : repliez un dossier, puis descendez aux flèches. Le focus doit sauter entièrement ses enfants. S'il atterrit sur un nœud invisible, le groupe est masqué en CSS au lieu de hidden.
  3. Le test de la feuille : focalisez un fichier (pas un dossier). Il ne doit pas être annoncé « réduit » ; sinon, aria-expanded traîne sur une feuille.
  4. Le test de sortie : un seul appui sur Tab doit faire sortir de l'arborescence entière, quelle que soit la position du focus.

Deux outils gratuits du site pour appuyer la vérification :

  • le testeur de focus visible vérifie que vos styles :focus restent perceptibles, condition nécessaire pour suivre le focus qui se déplace aux flèches de nœud en nœud ;
  • le simulateur de lecteur d'écran affiche l'arbre d'accessibilité : les rôles tree, treeitem, group, les états aria-expanded et le nom du conteneur s'y lisent directement.

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

Toutes les fiches composants accessibles