Role grid ou table : la grille interactive accessible RGAA
Role grid ou simple tableau HTML ? Table de décision, roving tabindex, clavier en deux dimensions et critères RGAA d'une grille interactive accessible.
Avant d'écrire role="grid", une seule question compte : l'utilisateur lit-il des données, ou les manipule-t-il cellule par cellule ? Toute l'accessibilité du motif se joue dans cette réponse.
<table> seul | <table role="grid"> | |
|---|---|---|
| Usage | Lire des données | Naviguer et interagir cellule par cellule |
| Clavier | Touches de lecture habituelles | Les flèches déplacent le focus de cellule en cellule |
| Mode lecteur d'écran | Mode lecture | Mode application |
| Exemples | Liste de prix, rapport | Tableur, tableau de données éditable |
Une grille (grid) est donc un widget composite tabulaire, navigable et opérable aux flèches : le motif d'un tableur ou d'un tableau éditable. La bonne base reste un vrai <table> avec ses <th scope>, auquel role="grid" ajoute le modèle d'interaction sans jeter les associations d'en-têtes. Une grille reconstruite en <div> perd ces associations et doit tout rebâtir, en moins bien.
Quand ce motif est le mauvais choix
C'est la première décision, et l'erreur la plus fréquente autour de ce motif :
- Tableau en lecture seule (liste de prix, rapport) : un
<table>ordinaire, sansrole="grid". Ajouterrole="grid"à un tableau statique est une dégradation : les lecteurs d'écran basculent en mode application et l'utilisateur doit apprendre une navigation aux flèches pour un contenu que ses touches de lecture géraient très bien. Aucun outil automatique ne signale ce défaut, car le balisage est techniquement valide. - Lignes dépliables mais cellules en lecture seule : un
<table>avec des boutons porteurs d'aria-expandedsuffit, pas de grille. - Liste d'options sélectionnables sans structure tabulaire : c'est un listbox, pas une grille. La grille devient le bon motif quand les options contiennent des actions à opérer.
- Plusieurs bibliothèques de tableaux posent
role="grid"par défaut : le retirer si le tableau est en lecture seule.role="grid"se justifie quand les cellules contiennent des contrôles à opérer, ou quand la navigation et la sélection cellule par cellule sont le cœur de l'usage.
Critères RGAA applicables
- Critère 5.6 : déclaration des en-têtes de tableau et critère 5.7 : association des cellules aux en-têtes. Une grille bâtie sur
<table>reste un tableau de données : les en-têtes restent déclarés en<th>avecscopeet les cellules restent associées à leurs en-têtes. Le rôle grid n'exonère pas de la thématique 5. - Critère 7.1 : scripts compatibles avec les technologies d'assistance. Les rôles
grid,row,gridcellet les états (sélectionaria-selected, triaria-sort) doivent être exposés et tenus à jour. - Critère 7.3 : scripts contrôlables au clavier et au pointeur. Les flèches doivent déplacer le focus dans les deux dimensions, avec Début / Fin et Page haut / Page bas.
- Critère 10.7 : prise de focus visible. La cellule focalisée doit être visiblement indiquée, et le mode édition d'une cellule doit avoir un style distinct du mode navigation.
- Critère 12.8 : ordre de tabulation cohérent. La grille est un unique arrêt de tabulation grâce au roving tabindex ; le script qui déplace ce tabindex doit maintenir un ordre cohérent (test 12.8.2).
- Critère 12.9 : absence de piège au clavier. Comme la grille capture toutes les flèches, Tab est la seule sortie : ne jamais l'intercepter.
Interaction clavier attendue
| Touche | Action |
|---|---|
| Flèches droite / gauche / bas / haut | Déplacent le focus d'une cellule dans la direction correspondante |
| Début (Home) / Fin (End) | Première / dernière cellule de la ligne courante |
| Ctrl + Début / Ctrl + Fin | Première cellule de la première ligne / dernière cellule de la dernière ligne |
| Page haut / Page bas | Saut de plusieurs lignes |
| Entrée ou F2 | Entre dans la cellule pour utiliser le contrôle qu'elle contient (mode édition) |
| Échap | Quitte le mode édition et revient à la navigation de grille |
| Tab | Sort de la grille (jamais capturé) |
Rôles et attributs ARIA
role="grid" se pose sur le <table>, avec un nom accessible : aria-labelledby pointant le <caption>, ou aria-label. Les rôles row et gridcell sont implicites, conservés par la structure de tableau. En option : aria-sort sur un en-tête trié, aria-selected pour la sélection, aria-readonly sur les cellules non éditables. aria-colcount et aria-rowcount ne servent qu'en cas de virtualisation : si toutes les lignes sont dans le DOM, le navigateur compte mieux qu'un attribut maintenu à la main.
<!-- Un vrai <table> : role="grid" ajoute l'interaction SANS jeter
la sémantique des en-têtes (critères 5.6 et 5.7). -->
<table role="grid" aria-labelledby="budget-caption">
<caption id="budget-caption">Budget T3 par service (éditable)</caption>
<thead>
<tr>
<th scope="col">Service</th>
<th scope="col">Budget</th>
</tr>
</thead>
<tbody>
<tr>
<!-- Les en-têtes de ligne restent des <th scope="row"> :
le rôle grid n'exonère pas de la thématique 5. -->
<th scope="row" tabindex="0">Ingénierie</th>
<!-- Roving tabindex : une seule cellule à tabindex="0",
toutes les autres à -1. -->
<td tabindex="-1">
<input type="text" value="120000" tabindex="-1"
aria-label="Budget Ingénierie">
</td>
</tr>
</tbody>
</table>
Le champ interne est en tabindex="-1" : la cellule est la cible de tabulation, et le champ ne devient utilisable qu'après Entrée. Cette bascule de mode empêche les flèches du champ texte et celles de la grille de se disputer les mêmes touches.
// La bascule de mode, côté script : en édition, la grille rend la main.
grille.addEventListener('keydown', (event) => {
if (enEdition) {
if (event.key === 'Escape') {
enEdition = false;
cellule.focus(); // retour à la navigation de grille
}
return; // flèches, Début, Fin : tout appartient au champ en édition
}
// ... navigation aux flèches entre cellules ...
// Tab : jamais géré. C'est la seule sortie de la grille (critère 12.9).
});
Dernier point de câblage : le focus doit se déplacer réellement (appel à .focus()), pas via aria-activedescendant, pour que l'indication visuelle de focus suive la cellule active (critère 10.7).
Et les deux contre-exemples qui résument les dérives du motif :
<!-- À NE PAS FAIRE : role="grid" sur un tableau en lecture seule.
Mode application imposé pour un contenu qui se lisait très bien. -->
<table role="grid">
<caption>Chiffre d'affaires par région</caption>
<tr><th scope="col">Région</th><td>2,4 M€</td></tr>
</table>
<!-- À NE PAS FAIRE : une grille en <div>.
Plus d'associations d'en-têtes (critères 5.6 et 5.7) : tout ce que
<table> donnait doit être rebâti à la main, en moins bien. -->
<div role="grid">
<div role="row"><div role="gridcell">Ingénierie</div></div>
</div>
Défauts fréquents et impact utilisateur
role="grid"sur un tableau en lecture seule : le défaut numéro un. Mode application imposé, navigation aux flèches obligatoire : un tableau lisible devient plus difficile à lire pour l'utilisateur de lecteur d'écran.- Tab intercepté par la grille : la grille possède déjà toutes les flèches ; si Tab est aussi capturé, l'utilisateur au clavier est enfermé sans aucune sortie, la pire forme de piège au clavier (critère 12.9).
- Contrôles internes tabbables sans bascule de mode : la cellule et son champ sont deux arrêts de tabulation, et une flèche dans le champ déplace à la fois le curseur texte et la cellule focalisée ; positionner son curseur devient impossible.
- Toutes les cellules en
tabindex="0": une grille de 20 lignes sur 10 colonnes devient 200 arrêts de tabulation ; traverser la page au clavier prend des minutes (critère 12.8). - En-têtes abandonnés « parce que c'est une grille » : sans
scope="row", une cellule est annoncée sans le service auquel elle appartient (critères 5.6 et 5.7). aria-rowcountmaintenu à la main sur une grille entièrement rendue : après un filtre, la grille annonce « ligne 3 sur 50 » dans une grille de 3 lignes.
Ce que les outils automatiques ne détectent pas
- Un
role="grid"posé sur un tableau en lecture seule : le choix du motif est une décision d'usage, pas de syntaxe. - Un Tab capturé (piège au clavier) ou des flèches qui se disputent une cellule en mode édition : seuls des tests au clavier le révèlent.
- La cohérence dynamique du roving tabindex et des états
aria-selected/aria-sortavec l'affichage.
Les outils automatiques, dont notre scanner, repèrent en revanche une grille sans nom accessible ou une structure de rôles invalide (enfants directs de grid qui ne sont pas des row) : un filet utile avant les vérifications manuelles qui suivent.
Vérifier ce composant
Protocole manuel, dans l'ordre où les défauts sont les plus probables :
- La question préalable : quelque chose dans les cellules est-il interactif ? Sinon, retirer
role="grid"et laisser vivre le tableau. C'est le défaut le plus courant. - Le test Tab (critère 12.9) : Tab pour entrer dans la grille, Tab à nouveau. On doit être sorti. Un seul arrêt de tabulation pour toute la grille.
- Le test du mode édition : entrer dans une cellule à champ texte (Entrée), puis Flèche gauche. Le curseur texte doit bouger, pas la cellule focalisée. Échap doit rendre la main à la grille.
- Le test des en-têtes : naviguer aux flèches vers une cellule du milieu ; le lecteur d'écran doit encore annoncer ses en-têtes de ligne et de colonne.
Deux outils gratuits du site aident sur ce composant : le Simulateur Lecteur d'Écran pour visualiser l'arbre d'accessibilité (rôles grid, row, gridcell, nom accessible, en-têtes), et le Testeur Focus Visible pour contrôler que la cellule active reste repérable au fil de la navigation (critère 10.7).
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