Le tableau paginé
Un tableau qui change de page se réagence, remonte en tête et ne dit rien de ce qui vient d'arriver. Trois défauts qu'on prend pour un rechargement, et qui n'en sont pas. Démo, code, critères RGAA.
5 critères RGAA en jeu
Essayez-le
Page 1 sur 2
Colonnes mesurées sur le contenu
| Personne | Rôle |
|---|---|
| Camille Durand c.durand@exemple.fr | Membre |
| a.b@exemple.fr | Membre |
| Léa Marchand lea@exemple.fr | Administratrice |
Première colonne : 0 px
Colonnes déclarées, contenu coupé
| Personne | Rôle |
|---|---|
| Camille Durand c.durand@exemple.fr | Membre |
| a.b@exemple.fr | Membre |
| Léa Marchand lea@exemple.fr | Administratrice |
Première colonne : 0 px
Changez de page, et regardez la largeur relevée sous chaque tableau. Celle du haut passe de quelques dizaines de pixels à presque toute la table, parce qu’une adresse longue est arrivée ; celle du bas ne bouge pas. Revenez en arrière : le premier tableau se réagence une seconde fois, en sens inverse. Regardez aussi la deuxième ligne du haut, qui n’a pas de nom : sa rangée est plus courte que ses voisines, et la colonne de droite cesse de s’aligner.
Cette démo existe aussi en page autonome.
L’erreur courante
Écrire le tableau, brancher la pagination, et s’arrêter là.
<!-- À ne pas faire : rien n'est déclaré, tout est mesuré -->
<table>
<tr>
<th scope="row">
{#if personne.nom}<span class="nom">{personne.nom}</span>{/if}
{personne.courriel}
</th>
<td>{personne.role}</td>
</tr>
</table> Le code est correct. Il produit pourtant, à chaque changement de page, quelque chose que les utilisateurs décrivent comme « la page recharge » — alors que rien n’a rechargé.
Trois causes se cumulent, et aucune ne se voit à la lecture :
- Les colonnes se remesurent. Sans largeur déclarée, le navigateur les répartit d’après le contenu réellement présent. Une seule adresse longue sur la page 2 redistribue toutes les colonnes, et la page 3 les redistribue encore. L’œil lit ce réagencement comme un nouveau document.
- La rangée change de hauteur selon ce qu’elle contient. Un nom facultatif rendu sous condition ajoute une ligne de texte quand il est là, et rien quand il ne l’est pas. Les rangées d’un même tableau n’ont alors pas toutes la même hauteur, et les colonnes de droite ne s’alignent plus d’une ligne à l’autre.
- La page remonte en tête. La plupart des routeurs remettent le défilement à zéro après une navigation — comportement juste quand on change d’écran, faux quand on reste sur le même et que seule une liste a changé. On clique « suivante » en bas du tableau, et l’écran saute en haut.
Comment le coder
1. Déclarer les colonnes plutôt que les mesurer
<table class="fixe">
<colgroup>
<col style="width: 70%" />
<col style="width: 30%" />
</colgroup>
</table> .fixe { table-layout: fixed; } Les deux vont ensemble ou pas du tout. table-layout: fixed sans <colgroup> répartit les colonnes à parts égales, ce qui est rarement voulu ; un <colgroup> sans disposition fixe ne sert à rien, le navigateur mesurant
quand même le contenu.
Le gain n’est pas seulement visuel : une disposition fixe se calcule sur la première rangée au lieu de lire toute la table, ce qui se sent sur les grandes listes.
2. Couper le texte, sans perdre la valeur
<span class="coupe" title={personne.courriel}>{personne.courriel}</span> .coupe {
display: block;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
} display: block n’est pas décoratif : sur une cellule de tableau, text-overflow est ignoré. C’est l’élément intérieur qui porte la coupe,
d’où le <span> là où l’on aurait écrit du texte nu.
Et c’est ici que se joue le critère 10.11. Une ellipse dit qu’il manque quelque
chose, jamais quoi : couper sans recours, c’est retirer du contenu de l’écran.
Le title est un premier filet — inégalement restitué, absent au toucher — il
ne suffit donc pas seul. La vraie réponse est que la valeur complète existe
ailleurs et à un clic : sur la fiche de l’élément, dans un export, dans le
résultat d’une recherche. Ne tronquez que ce qui est ainsi rattrapable, et
laissez respirer ce qui ne l’est pas.
3. Réserver la place d’une donnée facultative
<!-- Toujours rendue, vide s'il n'y a pas de nom -->
<span class="ligne">{personne.nom ?? ''}</span> .ligne {
display: block;
line-height: 1.4rem;
min-height: 1.4rem;
} Rendre la ligne sous condition fait varier la hauteur de la rangée. La rendre
toujours, vide au besoin, la fige. Un <span> vide ne porte aucun texte : il
n’ajoute rien à ce qu’un lecteur d’écran restitue, là où une espace insécable
— l’autre façon de tenir la place — s’ajouterait au nom accessible de chaque
rangée.
line-height et min-height à la même valeur : sans les deux, la boîte
vide et la boîte pleine ne tombent pas exactement à la même hauteur, et il
reste un demi-pixel d’écart entre deux rangées voisines.
Une réserve exprimée ainsi reste surchargeable, ce que demande le critère
10.12 : une feuille de style utilisateur qui impose line-height: 1.5 la
remplace, les rangées redeviennent inégales, et c’est très bien — l’utilisateur
a choisi. Ce qu’il ne faut pas faire, c’est enfermer la hauteur dans un height fixe sur la cellule, que rien ne desserre.
4. Ne pas remonter en tête, ne pas perdre le focus
<nav aria-label="Pagination des résultats" data-sveltekit-noscroll data-sveltekit-keepfocus> Deux comportements du routeur à désactiver, pour la même raison : on ne change pas d’écran, on met une liste à jour. Le défilement doit rester où il est, et le focus sur le lien qu’on vient d’activer.
Reste le cas que ce réglage ne couvre pas : le lien activé disparaît.
« Suivante » n’existe plus à la dernière page, « précédente » plus à la
première. Le focus retombe alors sur le corps du document, et la tabulation
suivante repart du haut de la page. Replacez-le sur le lien de la page devenue
courante — celui que porte déjà aria-current="page", et qui survit à tous les
rendus.
Ne le déplacez que dans ce cas. Bouger un focus encore bien posé est le défaut inverse, et il se remarque davantage.
5. Dire ce qui vient d’arriver
<p role="status">Résultats 6 à 10 sur 48 utilisateurs</p> Quand la navigation recharge la page, le changement s’annonce tout seul : nouveau titre de document, focus replacé. Quand la liste se met à jour en place, rien ne bouge pour un lecteur d’écran — le focus est resté sur le lien, et la grille a changé derrière.
La région doit exister dès le premier rendu, vide ou non. Une région créée au moment où l’on veut parler n’est pas restituée : les technologies d’assistance n’annoncent que ce qui change dans une région qu’elles surveillaient déjà.
Comptez sur le total, jamais sur la longueur de la page rendue : celle-ci est plus courte sur la dernière page, et l’annonce dirait « Résultats 1 à 3 » au lieu de « 46 à 48 ».
6. Garder le tableau atteignable quand il déborde
Une disposition fixe n’empêche pas un tableau d’être plus large que son
conteneur sur un écran étroit. Enveloppez-le dans une zone défilante qui porte tabindex="0", role="region" et un nom : une zone qui défile sans pouvoir
recevoir le focus ne se parcourt pas sans souris.
7. En avez-vous besoin ?
Une pagination sert à retrouver sa place et à revenir. Si la liste se lit d’un flux continu, un bouton « Afficher 20 résultats de plus » est plus simple et plus sûr au clavier. Si l’on doit pouvoir citer, partager ou imprimer un rang précis, la pagination et ses URL restent supérieures — voir la pagination pour le composant lui-même.
Au clavier
Le tableau n’est pas un composant interactif : il ne capte aucune touche. Ce sont ses abords qui se parcourent, et les raccourcis utiles sont ceux du mode tableau des lecteurs d’écran.
| Touche | Effet |
|---|---|
| Tab | entre dans la zone défilante, puis atteint les liens de pagination |
| ← → | fait défiler la zone horizontalement quand elle a le focus |
| Entrée | sur un lien de page, charge cette page ; le focus reste sur le lien, ou passe à la page courante s’il disparaît |
| Ctrl + Alt + flèches | déplace de cellule en cellule en mode tableau (NVDA, JAWS) |
| Ctrl + Alt + Origine | revient à la première cellule de la rangée (NVDA) |
Ce que le lecteur d’écran restitue
À l’arrivée sur une nouvelle page, la région d’annonce dit : « Résultats 6 à 10 sur 48 utilisateurs ». En parcourant ensuite les cellules, chaque valeur est précédée de son en-tête : « Personne, camille@exemple.fr », puis « Rôle, Membre ». La formulation varie d’un lecteur à l’autre, les informations non.
Le diagnostic inverse, dans l’ordre où l’on rencontre les défauts :
- Si le changement de page ne s’annonce pas, la région a été créée avec son
message au lieu d’exister avant lui, ou elle se trouve à l’intérieur d’un
élément portant
aria-busy="true", qui suspend les annonces de son sous-arbre. - Si l’on entend « lien » sans numéro, le complément masqué manque sur les liens de page.
- Si l’on n’entend que la valeur sans son en-tête, un
scopemanque. - Si l’on n’entend rien après avoir changé de page et que la tabulation repart
du haut du document, le focus est tombé sur
body: le lien activé a disparu et personne ne l’a rattrapé.
Checklist
- Déclarer les largeurs dans un
<colgroup>et posertable-layout: fixed— les deux ensemble. - Ne tronquer que ce dont la valeur complète reste atteignable ailleurs.
- Rendre les lignes facultatives toujours, vides au besoin, et réserver leur hauteur sans la verrouiller.
- Empêcher le retour en tête de page et la perte du focus au changement de page.
- Rattraper le focus seulement quand le lien activé a disparu.
- Annoncer les résultats atteints dans une région présente dès le premier rendu.
- Donner à chaque tableau une
captionet à chaquethunscope. - Envelopper le tableau dans une zone défilante nommée et atteignable au clavier.
Les critères RGAA en jeu
- Critère 10.11 — Contenus sans double défilement
Couper le texte d'une cellule à l'ellipse retire de l'information de l'écran. Le critère demande qu'aucun contenu ne soit perdu à 320 px ou à 200 % de zoom : une valeur tronquée doit rester atteignable autrement, sinon la colonne fixe se paie en contenu disparu.
- Critère 10.12 — Espacement du texte redéfinissable
Réserver la hauteur d'une ligne suppose un interlignage connu. Si cette valeur est écrite de telle sorte qu'un réglage d'espacement de l'utilisateur ne puisse plus la surcharger, le texte se chevauche ou se coupe dès qu'on l'augmente.
- Critère 5.4 — Titre de tableau associé
Chaque page du tableau est un nouveau rendu, et l'intitulé se perd facilement dans un composant qui redessine sa grille à chaque chargement. Sans caption, on ne sait plus ce qu'on parcourt.
- Critère 5.6 — En-têtes de tableau déclarés
Les colonnes déclarées poussent à écrire le balisage à la main, et un th sans scope est le premier oubli : les cellules ne sont plus reliées à rien, alors même que la grille est plus lisible qu'avant à l'œil.
- Critère 13.1 — Contrôle des limites de temps
Rien n'impose ici de limite de temps, mais une liste qui se rafraîchit toute seule sous les yeux de qui la parcourt en est une de fait : la page qu'on lisait a changé sans qu'on l'ait demandé.
Vérifiez votre composant avec l'extension
Pour aller plus loin
- La pagination — le composant de navigation lui-même, ses intitulés et sa troncature.
- Les tableaux de données —
caption,scope,headers: la sémantique de la grille. - Le tableau interactif — trier et filtrer sans perdre la restitution.
- L’espacement du texte — ce que le critère 10.12 attend d’une hauteur réservée.
- Le chargement et la progression — dire l’attente quand une liste met du temps.
Sources
- MDN —
table-layout— l’algorithme fixe, et ce qu’il cesse de mesurer - MDN —
text-overflow— pourquoi la coupe demande un bloc et un dépassement caché - WCAG Understanding — Reflow (1.4.10) — la perte de contenu à 320 px, le pourquoi normatif de la troncature
- WCAG Understanding — Text Spacing (1.4.12) — ce qu’une hauteur réservée doit laisser surcharger
- Adrian Roselli — Under-Engineered Responsive Tables — la zone défilante nommée et atteignable au clavier