Treegrid accessible : aria-level et quand un tableau suffit
Grille arborescente (treegrid) : deux modes clavier, aria-expanded sur la ligne, aria-level obligatoire, et l'alternative du tableau à lignes dépliables.
Une grille arborescente (treegrid) est une grille dont les lignes forment une hiérarchie dépliable, avec plusieurs colonnes et des cellules interactives : gestionnaire de fichiers en colonnes, arborescence budgétaire éditable. Aucun élément natif n'existe, et c'est le motif le plus complexe de l'ARIA APG : il croise la grille (navigation aux flèches en deux dimensions) et l'arborescence (dépliage hiérarchique), sans rien y ajouter d'autre que de la surface d'erreur.
À ne retenir qu'en dernier recours, donc. Son coût réel : deux modèles clavier, une bascule de mode, et des attributs de hiérarchie maintenus à la main.
Quand ce motif est le mauvais choix
Presque toutes les « treegrids » rencontrées en production auraient dû être autre chose :
| Ce que vous avez vraiment | Le bon motif |
|---|---|
| Hiérarchie sur une seule colonne de libellés | arborescence (treeview) |
| Données tabulaires plates avec cellules interactives | grille (grid) |
| Tableau en lecture seule dont certaines lignes se déplient | <table> + boutons aria-expanded dans une cellule |
| Hiérarchie ET plusieurs colonnes ET interaction au niveau de la cellule | grille arborescente (cette fiche) |
La troisième ligne est le cas que l'on manque le plus souvent : un tableau dépliable en lecture seule n'a besoin ni de role="treegrid", ni de mode application, ni de modèle aux flèches, et chaque utilisateur sait déjà s'en servir.
Critères RGAA applicables
- Critère 7.1 : scripts compatibles avec les technologies d'assistance. Les rôles
treegrid,row,gridcellet les étatsaria-expandedetaria-leveldoivent être exposés et tenus à jour, sans quoi la hiérarchie n'existe pas pour les technologies d'assistance. - Critère 7.3 : scripts contrôlables au clavier et au pointeur. Navigation aux flèches dans les deux dimensions, plus le dépliage et le repliage des lignes, le tout au clavier.
- Critère 10.7 : prise de focus visible. La ligne ou la cellule focalisée doit être visiblement indiquée, et les deux modes de focus (ligne, cellule) doivent se distinguer visuellement.
- Critère 10.8 : contenus cachés. Les lignes enfants repliées sont des contenus cachés destinés à être ignorés par les technologies d'assistance jusqu'à leur dépliage par une action utilisateur réalisable au clavier ; les masquer réellement (attribut
hiddenou retrait du DOM), pas seulement visuellement. - Critère 12.9 : absence de piège au clavier. Les flèches étant capturées par le widget, Tab doit rester la sortie et ne jamais être intercepté.
- Critère 5.6 : déclaration des en-têtes. Une grille arborescente rendue sous forme de
<table>reste un tableau de données : ses en-têtes de colonnes et de lignes restent déclarés en<th>.
Interaction clavier attendue
Deux modes de navigation, avec une bascule explicite :
| Touche | Action |
|---|---|
| Flèche bas / haut (mode ligne) | Passe d'une ligne visible à l'autre. |
| Flèche droite (mode ligne) | Déplie la ligne ; si elle est déjà dépliée, entre dans ses cellules. |
| Flèche gauche (mode ligne) | Replie la ligne, ou remonte au parent. |
| Entrée (mode ligne) | Entre dans les cellules de la ligne (bascule en mode cellule). |
| Flèche droite / gauche (mode cellule) | Passe d'une cellule à l'autre. |
| Flèche bas / haut (mode cellule) | Change de ligne dans la même colonne. |
| Échap (mode cellule) | Revient au mode ligne. |
| Début / Fin | Première / dernière ligne, ou première / dernière cellule de la ligne. |
| Ctrl + Début / Ctrl + Fin | Premier / dernier élément de la grille arborescente. |
| Tab | Sort du widget. Jamais capturé. |
Choisir un modèle et s'y tenir : si un même appui sur Flèche gauche replie la ligne ET déplace la cellule, le widget paraît cassé.
Rôles et attributs ARIA
role="treegrid" sur le conteneur (avec nom accessible), role="row" sur chaque <tr>, cellules en gridcell / rowheader / columnheader. aria-expanded se place sur la ligne (<tr>), jamais sur une cellule, et uniquement sur les lignes qui ont des enfants. aria-level, aria-posinset et aria-setsize se placent sur la ligne et sont indispensables : les <tr> ne peuvent pas s'imbriquer, le DOM est plat, donc rien d'autre ne porte la hiérarchie. C'est le seul motif où les maintenir à la main est la bonne pratique.
Mais d'abord, l'alternative à envisager sérieusement :
<!-- Un tableau à boutons de dépliage : pas de role treegrid, pas de
mode application, pas de modèle aux flèches. Pour une hiérarchie
en lecture seule, c'est le bon choix. -->
<table>
<caption>Budget du projet par service</caption>
<thead>
<tr><th scope="col">Service</th><th scope="col">Budget</th></tr>
</thead>
<tbody>
<tr>
<th scope="row">
<button type="button" aria-expanded="true" aria-controls="lignes-inge">
Ingénierie
</button>
</th>
<td>120 000 €</td>
</tr>
</tbody>
<!-- Lignes enfants dans leur propre tbody : une seule cible
pour aria-controls. -->
<tbody id="lignes-inge">
<tr><th scope="row">Front-end</th><td>70 000 €</td></tr>
<tr><th scope="row">Back-end</th><td>50 000 €</td></tr>
</tbody>
</table>
Et la vraie grille arborescente, quand le cumul hiérarchie + colonnes + interaction la justifie :
<table role="treegrid" aria-label="Fichiers du projet">
<thead>
<tr><th scope="col">Nom</th><th scope="col">Taille</th></tr>
</thead>
<tbody>
<!-- aria-expanded sur la LIGNE. aria-level, aria-posinset et
aria-setsize sont obligatoires : le DOM est plat, rien d'autre
ne porte la hiérarchie. -->
<tr role="row" aria-expanded="true" aria-level="1"
aria-posinset="1" aria-setsize="2" tabindex="0">
<th scope="row" tabindex="-1">src</th>
<td tabindex="-1">-</td>
</tr>
<tr role="row" aria-level="2" aria-posinset="1" aria-setsize="2"
tabindex="-1">
<th scope="row" tabindex="-1">index.js</th>
<td tabindex="-1">2,4 Ko</td>
</tr>
<!-- Ligne repliée : hidden, pas seulement invisible (critère 10.8). -->
<tr role="row" aria-expanded="false" aria-level="2" aria-posinset="2"
aria-setsize="2" tabindex="-1">
<th scope="row" tabindex="-1">composants</th>
<td tabindex="-1">-</td>
</tr>
<tr role="row" aria-level="3" aria-posinset="1" aria-setsize="1"
tabindex="-1" hidden>
<th scope="row" tabindex="-1">Bouton.js</th>
<td tabindex="-1">1,1 Ko</td>
</tr>
</tbody>
</table>
/* L'indentation est décorative : la hiérarchie réelle est aria-level. */
[role="treegrid"] tr[aria-level="2"] th { padding-inline-start: 1.5rem; }
[role="treegrid"] tr[aria-level="3"] th { padding-inline-start: 3rem; }
/* Les deux modes de focus doivent être visibles ET différents :
l'utilisateur doit savoir dans quel mode il se trouve (critère 10.7). */
[role="treegrid"] tr:focus-visible {
outline: 2px solid #0056b3;
outline-offset: -2px;
}
[role="treegrid"] td:focus-visible,
[role="treegrid"] th:focus-visible {
outline: 2px dashed #1a7f37;
outline-offset: -2px;
}
// Au dépliage, ne révéler que les enfants directs : les petits-enfants
// dont le parent est encore replié restent masqués.
function deplierLigne(ligne, deplie) {
ligne.setAttribute('aria-expanded', String(deplie));
const niveau = Number(ligne.getAttribute('aria-level'));
for (const enfant of lignesEnfants(ligne)) {
const niveauEnfant = Number(enfant.getAttribute('aria-level'));
if (niveauEnfant === niveau + 1) enfant.hidden = !deplie;
else if (!deplie) enfant.hidden = true;
}
}
Les contre-exemples du motif, tous plus fréquents que la version correcte :
<!-- À ne pas faire : aria-expanded sur une cellule. L'erreur la plus
courante du motif : le mauvais élément est annoncé dépliable, la
ligne elle-même n'annonce aucun état. -->
<tr role="row">
<th scope="row" aria-expanded="true">src</th>
</tr>
<!-- À ne pas faire : omettre aria-level. Le DOM est plat : sans lui, la
grille est annoncée comme un tableau plat, la hiérarchie disparaît
(critère 7.1). -->
<tr role="row" aria-expanded="true" tabindex="0">
<th scope="row">src</th>
</tr>
<!-- À ne pas faire : aria-expanded="false" sur une ligne feuille.
Elle prétend avoir des enfants qu'elle n'a pas : l'utilisateur
tente de la déplier en boucle. -->
<tr role="row" aria-expanded="false" aria-level="2">
<th scope="row">index.js</th>
</tr>
<!-- À ne pas faire : replier en CSS seulement. Les lignes restent dans
l'arbre d'accessibilité et dans le parcours aux flèches
(critère 10.8). Utiliser hidden. -->
<tr role="row" style="visibility: collapse">...</tr>
<!-- À ne pas faire : role="treegrid" sur un tableau dépliable en
lecture seule. Mode application et double modèle clavier imposés
pour ce qu'un <table> à boutons de dépliage sert sans apprentissage. -->
<table role="treegrid">
<tr role="row" aria-expanded="false">
<th scope="row">Ingénierie</th><td>120 000 €</td>
</tr>
</table>
Défauts fréquents et impact utilisateur
aria-expandedsur une cellule au lieu de la ligne : l'erreur la plus courante du motif. Le mauvais élément est annoncé dépliable, et la ligne elle-même n'annonce aucun état ; l'utilisateur de lecteur d'écran ne sait ni quoi déplier ni si c'est fait.aria-levelabsent : le DOM étant plat, la grille arborescente est annoncée comme un tableau plat ; toute la hiérarchie disparaît et le motif perd sa raison d'être (critère 7.1).aria-expanded="false"sur des lignes feuilles : chaque feuille prétend avoir des enfants ; l'utilisateur tente de déplier des lignes vides en boucle.- Lignes repliées masquées seulement en CSS (hauteur nulle,
visibility: collapse) : elles restent dans l'arbre d'accessibilité et dans le parcours aux flèches ; l'utilisateur navigue dans des lignes invisibles à l'écran (critère 10.8). aria-posinset/aria-setsizecalculés une seule fois au chargement : après une insertion ou un filtre, le lecteur d'écran annonce avec assurance « 3 sur 7 » dans une branche qui en compte 4.role="treegrid"sur un tableau dépliable en lecture seule : mode application et double modèle clavier imposés pour un contenu qu'un<table>avec boutons de dépliage servait sans aucun apprentissage.
Ce que les outils automatiques ne détectent pas
- Un
aria-expandedposé sur la cellule plutôt que sur la ligne, des niveauxaria-levelabsents ou faux, des compteursaria-posinset/aria-setsizepérimés : autant d'états sémantiques que seul un test au lecteur d'écran révèle. - Le choix du motif lui-même : une grille arborescente qui aurait dû être un tableau à lignes dépliables est syntaxiquement irréprochable.
- La séparation des deux modes clavier et la sortie par Tab : vérifications entièrement manuelles.
Les outils automatiques, dont notre scanner, repèrent en revanche une structure de rôles invalide ou un widget sans nom accessible : nécessaire, très loin d'être suffisant sur ce motif.
Vérifier ce composant
Protocole manuel, dans cet ordre :
- La première question, avant tout test : fallait-il un treegrid ? Lignes en lecture seule : un
<table>à boutons de dépliage. Une seule colonne : une arborescence. Lignes plates : une grille. Un motif correct n'est pas pour autant le bon motif. - Le test de placement d'
aria-expanded: focaliser une ligne dépliable au lecteur d'écran. C'est la LIGNE qui doit annoncer « déplié », pas une cellule. - Le test de hiérarchie : focaliser une ligne imbriquée. Attendre « niveau 2, 1 sur 2 » ; sans annonce de niveau,
aria-levelmanque et rien d'autre ne porte la hiérarchie. - Le test des modes : en mode ligne, Flèche gauche replie ; en mode cellule, elle déplace la cellule. Si un même appui fait les deux, les modes ne sont pas séparés. Et Tab doit toujours sortir du widget (critère 12.9).
Deux outils gratuits du site aident à ce contrôle : le Testeur Focus Visible vérifie que vos styles :focus distinguent bien les deux modes (critère 10.7), et le Simulateur Lecteur d'Écran affiche l'arbre d'accessibilité : rôles treegrid / row / gridcell, états aria-expanded et niveaux aria-level 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