Le tableau interactif
Trier, filtrer, paginer : trois ajouts sur un tableau de données, et trois façons de perdre l'information en route. Le tri vit dans un bouton et dans aria-sort ; le filtre, dans un message de statut. Démo, code, critères RGAA.
5 critères RGAA en jeu
Essayez-le
| Statut | ||
|---|---|---|
| 1.1 | Images | Conforme |
| 1.3 | Images | Non conforme |
| 3.2 | Couleurs | Conforme |
| 5.7 | Tableaux | Non applicable |
| 7.1 | Scripts | Non conforme |
| 8.9 | Éléments obligatoires | Conforme |
| 11.1 | Formulaires | Non conforme |
| 12.8 | Navigation | Conforme |
Tabulez jusqu’à un en-tête : le focus s’y arrête, parce que c’est un bouton. Activez-le deux fois, l’ordre s’inverse — et le focus ne bouge pas, seules les lignes se réordonnent dessous. Tapez ensuite dans le champ de filtre : le compte au-dessus du tableau change, et c’est lui qu’un lecteur d’écran annonce.
Cette démo existe aussi en page autonome.
L’erreur courante
L’en-tête cliquable. Il a l’air d’un bouton, il en a le curseur, il n’en a rien d’autre.
<!-- À ne pas faire : le tri n'existe que pour la souris -->
<th scope="col" class="triable" onclick="trier('critere')">
Critère <span class="fleche">▲</span>
</th> Un <th> ne prend pas le focus, ne répond ni à Entrée ni à
Manque aussi l’état. La flèche dit à l’œil quelle colonne est triée et dans
quel sens ; aux technologies d’assistance, elle ne dit rien d’utile — au mieux
le nom du caractère, au pire rien. Sans aria-sort sur le <th>, un tableau
trié et un tableau non trié se restituent exactement pareil.
Trois variantes de la même perte :
- La commande sortie du tableau — une liste déroulante « Trier par… » au-dessus.
Elle fonctionne au clavier, mais la colonne et sa commande ne sont plus liées,
et c’est toujours sur le
<th>qu’un lecteur d’écran cherche l’état du tri. - Le
<th>changé en<td>pour y loger le bouton plus commodément. La colonne perd son en-tête, et chaque cellule perd son étiquette : ce n’est plus un défaut de tri, c’est un défaut de tableau. - Le filtre qui remplace le
<tbody>en silence. À l’écran, quinze lignes ont disparu. Pour qui ne voit pas, rien ne s’est passé, et il faut relire tout le tableau pour comprendre ce que la saisie a fait.
Le code, pas à pas
1. D’abord un tableau de données, et rien d’autre
<table>
<caption>Statut des critères audités</caption>
<thead>
<tr>
<th scope="col">Critère</th>
<th scope="col">Thématique</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">1.1</th>
<td>Images</td>
</tr>
</tbody>
</table> Le tri, le filtre et la pagination s’ajoutent par-dessus cette structure ;
ils n’en dispensent d’aucune part. caption, <th> et scope restent exigés
à l’identique — voir les tableaux de données pour le détail, y compris headers en double entrée.
C’est la règle qui décide de tout le reste : ce qu’on ajoute ne doit rien retirer.
2. Le tri : un bouton dans le <th>
<th scope="col" aria-sort="ascending">
<button type="button" onclick="trier('critere')">
Critère <span aria-hidden="true">▲</span>
</button>
</th> Le bouton se place dans l’en-tête, jamais à sa place : la colonne garde son <th> et son scope, et gagne une commande focalisable qui répond à
aria-sort va sur le <th>, pas sur le bouton. C’est la colonne qui est
triée, pas la commande ; posé sur le bouton, l’attribut n’est pas restitué
comme un état de colonne et le tri redevient invisible.
Trois valeurs seulement comptent ici : ascending, descending, et none pour une colonne triable qui ne l’est pas en ce moment. Une colonne qu’on ne
trie jamais ne porte pas l’attribut du tout — none y annoncerait un tri
possible qui n’existe pas.
La flèche double l’état pour l’œil. Elle est aria-hidden="true" : sans quoi
elle s’ajoute à l’annonce, en doublon et sans nom utile.
3. Une seule colonne triée à la fois
for (const entete of entetes) {
entete.setAttribute(
'aria-sort',
entete === triee ? (croissant ? 'ascending' : 'descending') : 'none',
);
} Remettre les autres à none fait partie du tri. Deux colonnes qui se déclarent
triées en même temps, c’est le défaut classique de ce composant : la classe CSS
a bougé, l’attribut est resté — et l’information devient fausse au lieu d’être
seulement absente.
4. Le nom du bouton, si vous l’enrichissez
Le libellé de la colonne suffit : aria-sort porte déjà l’état, et « Trier par
critère, ordre croissant » le répète. Si vous tenez à un nom plus explicite, le
test 7.1.3 fixe la contrainte : le nom accessible doit contenir au moins
l’intitulé visible. aria-label="Trier par critère" sur un bouton intitulé
« Critère » convient ; aria-label="Ordonner la colonne" ne convient pas, et
laisse une commande vocale au nom visible sans effet.
5. Le filtre : un message de statut
<!-- Présent dès le premier rendu, vide -->
<p role="status"></p> statut.textContent = `${visibles.length} lignes sur ${total.length}`; Le filtrage ne modifie rien là où l’utilisateur se trouve : il fait varier un
nombre de lignes plus bas dans la page. Le critère 7.5 demande que ce genre de
résultat passe par un message de statut — role="status" pour le résultat d’une action, poli, qui attend une pause dans
la lecture au lieu de couper la parole.
Deux détails qui décident du fonctionnement :
- La région existe avant l’annonce. Créée au moment du filtrage, elle n’est pas restituée : le lecteur d’écran n’observe que les régions qu’il connaissait déjà.
- Le message attend une pause de frappe. Recalculé à chaque touche, il couvre la saisie au lieu de l’informer. Quelques centaines de millisecondes suffisent à n’annoncer que le résultat.
6. La pagination, et le focus qu’elle emporte
Paginer se traite comme dans la pagination, avec un
piège propre à la mise à jour en place : le lien ou le bouton qu’on vient
d’activer peut ne plus exister après le rendu — « Page suivante » disparaît à la
dernière page. Le focus retombe alors sur body, et l’on ne sait plus ni où
l’on est ni d’où repartir. Reposez-le sur un élément qui survit au rendu.
7. Combien de lignes avant d’en arriver là ?
Sur trente lignes, le tri et le filtre coûtent plus qu’ils ne rapportent : la recherche du navigateur (Ctrl + F) trouve déjà, et elle ne demande aucun code. Trier côté serveur avec des liens dans les en-têtes est l’autre réponse : chaque ordre a une URL, il se partage, il s’imprime, et le changement de page annonce le nouvel état tout seul par le titre du document.
Au clavier
| Touche | Effet |
|---|---|
| Tab | Va au champ de filtre, puis à chaque bouton d’en-tête triable. Les cellules ne reçoivent pas le focus. |
| Entrée / Espace | Trie sur la colonne dont l’en-tête a le focus. Le focus reste sur le bouton ; seules les lignes se réordonnent. |
| Ctrl + Alt + flèches | Déplace de cellule en cellule (NVDA, JAWS) ; l’en-tête est réannoncé au changement de colonne, avec son état de tri. |
| T | Va au tableau suivant de la page (mode navigation rapide). |
Les flèches seules ne servent à rien ici : un tableau triable reste une suite de
commandes indépendantes, pas une grille à parcourir au clavier. Le motif grid de l’ARIA existe pour cela, et il coûte bien plus cher — ne l’ouvrez que si vos
cellules sont elles-mêmes interactives.
Ce que le lecteur d’écran restitue
Sur un en-tête triable : le nom, la nature, l’état — de l’ordre de « Critère, bouton, colonne triée par ordre croissant ». Après filtrage, la région de statut prend la parole seule : « 4 lignes sur 8, tri par critère, ordre croissant ». Les formulations varient d’un lecteur à l’autre ; les informations restituées, non.
Si vous n’entendez pas « bouton » sur l’en-tête, c’est un <th> cliquable : le
tri n’existe pas au clavier. Si vous n’entendez rien sur l’ordre, aria-sort manque, ou il est posé sur le bouton au lieu du <th>. Si le filtrage ne
produit aucune annonce, la région role="status" n’existe pas — ou elle a été
créée au moment de l’annonce. Et si une valeur est lue sans son en-tête, le
défaut n’est pas dans l’interaction mais dans le tableau lui-même.
Checklist
- Le tableau reste un tableau de données :
caption,<th>,scope,headers. - Un
<button type="button">dans le<th>, jamais un<th>cliquable. aria-sortsur le<th>, pas sur le bouton.- Une seule colonne porte
ascendingoudescending; les autres triables,none. - Le nom accessible du bouton contient l’intitulé visible de la colonne.
- Une région
role="status", présente dès le premier rendu, qui annonce le résultat du filtre. - La flèche de tri masquée par
aria-hidden— elle double l’état, elle ne le porte pas.
Les critères RGAA en jeu
- Critère 5.6 — En-têtes de tableau déclarés
Un en-tête reste un th même quand il porte un bouton de tri. Le remplacer par un td pour y loger la commande fait perdre son en-tête à toute la colonne.
- Critère 5.7 — Association des cellules aux en-têtes
Trier et filtrer ne dispense pas d'associer chaque cellule à ses en-têtes : une valeur restituée sans le sien reste illisible, quel que soit l'ordre des lignes.
- Critère 7.1 — Compatibilité des scripts
Le tri est un état de colonne : sans aria-sort sur le th, rien n'expose quelle colonne est triée ni dans quel sens — la flèche ne parle qu'à l'œil.
- Critère 7.3 — Contrôle des scripts au clavier
Trier doit se faire au clavier, ce qu'un bouton dans l'en-tête donne et qu'un th porteur d'un gestionnaire de clic retire.
- Critère 7.5 — Messages de statut
Filtrer retire des lignes ailleurs dans la page, sans rien changer sous le curseur : le nombre de résultats doit être annoncé par un message de statut.
Vérifiez votre composant avec l'extension
Pour aller plus loin
- Les tableaux de données : la structure sur laquelle tout ceci se pose, et qu’aucune interaction ne dispense d’écrire.
- Critère 7.5 — Messages de statut : ce que vaut une mise à jour de contenu qui ne s’annonce pas.
- La pagination : le troisième ajout, et le focus à rattraper quand la liste change sans recharger la page.
- « Message de statut » au glossaire : la
définition qui décide de ce qui relève de
statusou d’alert.
Sources
- ARIA APG — Sortable Table Example — le contrat de comportement du tri
- MDN —
aria-sort— les valeurs de l’attribut et leur portée - WCAG — Understanding 4.1.3 Status Messages — pourquoi un changement de contenu doit s’annoncer
- Adrian Roselli — Sortable Table Columns — les limites de restitution, mesurées lecteur par lecteur