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
| Besoin | Solution 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égorie | listbox ou <select> |
| Explorateur de fichiers : centaines de nœuds, déplier/replier, sélection pilotée au clavier | arborescence 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
- Critère 7.1 : compatibilité des scripts avec les technologies d'assistance. Les rôles
tree,treeitem,groupet les étatsaria-expandedouaria-selecteddoivent être exposés via l'API d'accessibilité, car rien n'est natif dans ce motif. - Critère 7.3 : contrôle au clavier et au pointeur. La navigation par flèches, le dépliage et le repliage doivent être entièrement réalisables au clavier comme au pointeur.
- Critère 10.7 : prise de focus visible. Le nœud qui reçoit le focus au fil de la navigation par flèches doit rester visuellement repérable.
- Critère 10.8 : contenus cachés. Une branche repliée est un contenu caché qui doit être réellement ignoré des technologies d'assistance (attribut
hiddenoudisplay: none), en cohérence avec son état à l'écran. - Critère 12.8 : ordre de tabulation cohérent. Le roving tabindex maintient un point d'entrée unique et un ordre de focus cohérent, y compris après une mise à jour de l'arborescence par script.
- Critère 12.9 : absence de piège au clavier. La touche Tab doit toujours permettre de quitter l'arborescence ; les flèches servent à naviguer à l'intérieur.
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.
| Touche | Action |
|---|---|
| Tab / Maj + Tab | Entre dans l'arborescence (un seul arrêt de tabulation), puis en sort |
| Flèche bas / Flèche haut | Nœud visible suivant / précédent, sans jamais entrer dans les branches repliées |
| Flèche droite | Dé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 gauche | Replie un nœud ouvert ; depuis un enfant, remonte au parent |
| Début / Fin | Premier / dernier nœud visible |
| Entrée | Active le nœud focalisé |
| Saisie de caractères | Type-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-setsizeetaria-posinsetmaintenus à 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-expandedposé 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 :
- 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.
- 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. - Le test de la feuille : focalisez un fichier (pas un dossier). Il ne doit pas être annoncé « réduit » ; sinon,
aria-expandedtraîne sur une feuille. - 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
:focusrestent 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 étatsaria-expandedet 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