RGAA Lab

Composants accessibles

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 des critères audités
Statut
1.1ImagesConforme
1.3ImagesNon conforme
3.2CouleursConforme
5.7TableauxNon applicable
7.1ScriptsNon conforme
8.9Éléments obligatoiresConforme
11.1FormulairesNon conforme
12.8NavigationConforme

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 à

Espace, et ne s'annonce pas comme une commande. Le tri est donc purement à la souris — et personne n'apprend qu'il existe.

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 à

Entrée et Espace sans qu'on écrive une ligne de clavier.

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 statutrole="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

ToucheEffet
TabVa au champ de filtre, puis à chaque bouton d’en-tête triable. Les cellules ne reçoivent pas le focus.
Entrée / EspaceTrie 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èchesDéplace de cellule en cellule (NVDA, JAWS) ; l’en-tête est réannoncé au changement de colonne, avec son état de tri.
TVa 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-sort sur le <th>, pas sur le bouton.
  • Une seule colonne porte ascending ou descending ; 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

Vérifiez votre composant avec l'extension

Pour aller plus loin

Sources