L'arbre
C'est le composant le plus lourd en ARIA, et celui dont on a le moins souvent besoin. Une liste imbriquée avec des sections dépliables fait le même travail sans un seul rôle — voici quand l'arbre se justifie vraiment, et comment le coder alors.
5 critères RGAA en jeu
Essayez-le
- export-audit
- pages
- accueil.html
- recherche.html
- contact.html
- captures
- 1-1-accueil.png
- 7-1-menu-deroulant.png
- rapport.html
- pages
- modeles
- grille-106-criteres.csv
Ce que porte le nœud qui a le focus
Posez le focus sur un nœud de l’arbre : ses attributs réels s’affichent ici.
Tabulez : l’arbre entier ne coûte qu’un seul arrêt, quel que soit le nombre de fichiers. Puis lâchez la tabulation et prenez les flèches : ↓ descend d’une ligne visible, → déplie puis entre, ← replie puis remonte. Le relevé à droite lit les attributs réels du nœud focalisé — et montre que son niveau n’est déclaré nulle part.
Cette démo existe aussi en page autonome.
L’erreur courante
Un menu de navigation à deux étages, à qui l’on colle des rôles d’arbre parce que ça y ressemble.
<!-- À ne pas faire : ce n'est pas un arbre, c'est un menu -->
<ul role="tree">
<li role="treeitem"><a href="/criteres">Les critères</a></li>
<li role="treeitem" aria-expanded="false">
<a href="/composants">Les composants</a>
<ul role="group">
<li role="treeitem"><a href="/composants/accordeon">L'accordéon</a></li>
</ul>
</li>
</ul> Ces rôles ne décrivent pas la page, ils promettent un comportement. Un lecteur d’écran annonce « arborescence, élément d’arborescence, réduit, niveau 1, 2 sur 2 » : l’utilisateur en déduit qu’il doit prendre les flèches, il les prend, et rien ne bouge. Avant les rôles, il entendait des liens dans une liste et savait quoi faire.
La faute de structure est aussi lourde. Le nœud focalisable d’un arbre, c’est le treeitem lui-même. Ici, le focus va au lien qu’il contient : chaque ligne
devient un arrêt de tabulation, le tabindex roulant est impossible, et un
lecteur d’écran annonce un lien focalisé à l’intérieur d’un élément
d’arborescence qui ne l’est pas.
Trois variantes du même malentendu :
aria-expandedposé sur le<ul role="group">plutôt que sur le nœud parent. L’état existe dans le DOM, mais il est sur un élément qui ne prend jamais le focus : personne ne l’entend jamais.- Le sous-arbre replié en
height: 0. Ses nœuds restent focalisables : le focus quitte l’écran, parcourt une arborescence invisible, puis réapparaît plus bas. role="tree"sans aucun clavier. Le composant se manipule à la souris, se décrit correctement, et reste inutilisable pour qui ne l’a pas.
Le code, pas à pas
1. Des listes imbriquées, d’abord
Écrivez la structure avant les rôles, et vérifiez qu’elle tient sans eux.
<ul>
<li>
export-audit
<ul>
<li>rapport.html</li>
</ul>
</li>
</ul> Ce document est déjà correct : la hiérarchie est portée par l’imbrication, un lecteur d’écran annonce les listes et leur nombre d’éléments. Tout ce que les rôles ajouteront, c’est le pliage et la navigation aux flèches. S’ils ne sont pas nécessaires, vous avez fini.
2. Les trois rôles
<ul role="tree" aria-label="Fichiers de l'export d'audit">
<li role="treeitem" aria-expanded="true" tabindex="0">
<span>export-audit</span>
<ul role="group">
<li role="treeitem" tabindex="-1"><span>rapport.html</span></li>
</ul>
</li>
</ul> role="tree" déclare l’arbre, role="treeitem" chaque nœud, role="group" chaque sous-arbre. Le aria-label du tree compte : sans lui, on entend
« arborescence » sans savoir de quoi.
role="group" n’est pas décoratif, et il rend un service qu’on ne soupçonne
pas. Le nom accessible d’un treeitem se calcule à partir de son contenu — or
son contenu inclut tout le sous-arbre. Comme le nom d’un group ne se calcule
pas à partir du contenu, le sous-arbre est écarté du calcul, et le nœud s’appelle
« export-audit » plutôt que « export-audit rapport.html ». Remplacez le group par un <ul> nu et le nom du nœud grossit à chaque fichier ajouté.
3. aria-expanded va sur le nœud, jamais sur le groupe
<li role="treeitem" aria-expanded="false" tabindex="-1">
<span>captures</span>
<ul role="group" hidden>…</ul>
</li> L’état appartient à l’élément qui prend le focus, parce que c’est en arrivant
dessus qu’on l’entend. Sur le <ul role="group">, il est exact et muet.
Un nœud sans enfants ne porte pas aria-expanded, même à false :
l’attribut annoncerait « réduit » sur une feuille, et laisserait croire qu’il y
a quelque chose à déplier.
Le sous-arbre replié se masque par hidden, qui le retire de l’affichage et de
l’ordre de tabulation d’un seul attribut. Une hauteur nulle ne fait que la
moitié du travail — la moitié visible.
4. Le niveau : ce que vous n’avez pas à déclarer
aria-level, aria-setsize et aria-posinset existent, et c’est ce qui fait
la réputation de ce composant. Ils ne servent que si la structure du DOM ne
reflète pas l’arborescence : des nœuds à plat, indentés au CSS, comme le font
certaines grilles virtualisées où seules les lignes visibles existent.
Quand chaque role="group" est bien à l’intérieur du treeitem qui le gouverne,
le navigateur calcule le niveau, le rang et le total tout seul. Le relevé de la
démo le montre : rien n’est déclaré, et le niveau est juste. Trois attributs de
moins à tenir à jour, c’est trois occasions de moins de mentir.
5. Un seul arrêt de tabulation, quatre flèches
Le nœud focalisé porte tabindex="0", tous les autres tabindex="-1". On entre
dans l’arbre d’une tabulation, on en sort d’une autre, et entre les deux, ce sont
les flèches qui déplacent le focus.
function surTouche(evenement, noeud) {
const visibles = [...arbre.querySelectorAll('[role="treeitem"]')].filter(
(candidat) => !candidat.closest('ul[hidden]'),
);
const rang = visibles.indexOf(noeud);
const deplie = noeud.getAttribute('aria-expanded') === 'true';
if (evenement.key === 'ArrowRight') {
if (deplie) focaliser(visibles[rang + 1]);
else deplier(noeud);
} else if (evenement.key === 'ArrowLeft') {
if (deplie) replier(noeud);
else focaliser(noeud.parentElement.closest('[role="treeitem"]'));
}
// ↓ et ↑ : visibles[rang ± 1]. Début et Fin : le premier et le dernier.
evenement.preventDefault();
} Deux points valent qu’on s’y arrête.
La liste des nœuds parcourables se relit dans le DOM, filtrée sur ce qui n’est pas dans un groupe masqué. Un tableau parallèle tenu à la main diverge dès le premier pliage, et le focus part alors sur un nœud invisible.
→ et ← font chacune deux choses, mais jamais les deux à la fois : déplier ou entrer, replier ou remonter. C'est ce qui rend la descente prévisible — on ne saute jamais deux niveaux d'une pression.Le preventDefault() n’est pas optionnel : sans lui, la flèche déplace le focus et fait défiler la page.
6. Avant de coder ça : en avez-vous besoin ?
C’est le composant le plus coûteux de cette collection, et celui qui se justifie le plus rarement. Trois questions le tranchent.
- Combien de niveaux ? À deux niveaux, un accordéon par section fait le même travail : un bouton natif, un état annoncé, aucun rôle. L’arbre commence à payer vers trois ou quatre niveaux réels.
- Navigue-t-on, ou lit-on ? Un menu de site se lit et se clique : ce sont des liens dans une liste. Un explorateur de fichiers se parcourt : c’est un arbre.
- Faut-il lier un nœud, l’imprimer, le trouver par la recherche du navigateur ? Un arbre ne met rien dans l’URL et masque tout le reste.
Une liste imbriquée dont chaque branche est un <details> couvre la plupart des
besoins, sans un seul rôle ARIA ni une ligne de gestion du focus.
Au clavier
| Touche | Effet |
|---|---|
| Tab | Entre dans l’arbre (un seul arrêt), puis en sort vers la suite de la page. |
| ↓ ↑ | Va au nœud visible suivant ou précédent, en traversant les niveaux. |
| → | Déplie le nœud si le focus est sur un nœud replié ; sinon, va au premier enfant. |
| ← | Replie le nœud si le focus est sur un nœud déplié ; sinon, remonte au parent. |
| Début / Fin | Va au premier ou au dernier nœud visible. |
| Une lettre | Va au nœud visible suivant dont le nom commence par cette lettre. |
| Entrée / Espace | Sélectionne le nœud, ou déplie le dossier. |
Ce que le lecteur d’écran restitue
Sur un nœud : le nom de l’arbre, la nature, le nom du nœud, son état, son niveau et son rang — de l’ordre de « Fichiers de l’export d’audit, arborescence, captures, élément d’arborescence, réduit, niveau 2, 2 sur 3 ». La formulation varie d’un lecteur à l’autre ; ces informations, non.
Le diagnostic inverse se fait sur ce qui manque :
- Pas de niveau ni de rang annoncés : les
role="group"ne sont pas dans les nœuds qu’ils prolongent, l’imbrication est fausse. - Le nom du nœud suivi de tous ses fichiers : le sous-arbre entre dans le nom
accessible, il manque un
role="group". - « réduit » sur un fichier :
aria-expandedtraîne sur une feuille. - « lien » ou « bouton » à la place d’« élément d’arborescence » : le focus va à un élément interactif contenu dans le nœud, pas au nœud.
Checklist
- Vérifier d’abord qu’un accordéon ou des
<details>imbriqués ne suffisent pas. role="tree"nommé,role="treeitem",role="group"— les trois, ou aucun.- Le
treeitemest l’élément focalisable : rien d’interactif à l’intérieur. aria-expandedsur le nœud parent, absent sur les feuilles.- Le sous-arbre replié masqué par
hidden, jamais par une hauteur nulle. - Un seul
tabindex="0"dans tout l’arbre, les autres à-1. - Les quatre flèches, Début et Fin, avec
preventDefault(). aria-leveletaria-posinsetseulement si le DOM ne reflète pas la hiérarchie.
Les critères RGAA en jeu
- Critère 7.1 — Compatibilité des scripts
Rien en HTML ne fait un arbre : les rôles tree, treeitem et group et l'état aria-expanded sont tout ce que la technologie d'assistance recevra — et un role=tree posé sur un menu ordinaire lui fait annoncer un composant qui n'existe pas.
- Critère 7.3 — Contrôle des scripts au clavier
Un arbre annonce des flèches. Si elles ne sont pas câblées, plus rien ne se parcourt au clavier : le tabindex roulant a déjà retiré les nœuds de la tabulation.
- Critère 12.8 — Cohérence de l’ordre de tabulation
Un seul nœud entre dans l'ordre de tabulation. Sans tabindex roulant, une arborescence de quarante fichiers coûte quarante tabulations avant d'atteindre la suite de la page.
- Critère 10.8 — Contenus cachés ignorés
Un sous-arbre replié doit être caché pour tout le monde. Seulement réduit en CSS, ses nœuds restent focalisables et le clavier tombe dans une arborescence invisible.
- Critère 12.10 — Raccourcis clavier à une seule touche
La recherche à l'initiale est un raccourci à une seule touche. Lié au nœud qui a le focus, il est conforme ; posé sur le document, il capture les frappes de toute la page et bloque la saisie partout ailleurs.
Vérifiez votre composant avec l'extension
Pour aller plus loin
- L’accordéon : le pliage à un niveau, sans aucun rôle ARIA — la réponse juste dans la plupart des cas où l’on pense à un arbre.
- Le menu de navigation : ce qu’est vraiment une navigation à deux étages, et pourquoi ce n’est pas un arbre.
- Critère 12.8 — Cohérence de l’ordre de tabulation : ce que le tabindex roulant protège.
- Critère 10.8 — Contenus cachés ignorés et « contenu caché » au glossaire : la liste limitative des façons de masquer un sous-arbre.
Sources
- ARIA APG — Tree View Pattern — le contrat de comportement, clavier compris
- MDN — ARIA
treerole — la référence des trois rôles et de leurs attributs - WCAG — Understanding 4.1.2 Name, Role, Value — pourquoi rôle et état doivent être exposés
- ARIA APG — Navigation Treeview Example — une implémentation complète à lire