Chapitre 3.5 — Tableaux de données
⏱️ TL;DR — Un tableau de données légitime se code avec
<table>, un<caption>qui le nomme,<thead>/<tbody>pour séparer en-tête et corps, et surtout des<th scope="col|row">pour déclarer les en-têtes. Grâce àscope, le lecteur d’écran rappelle l’en-tête à chaque cellule (« Note, 15 ») : sans lui, une grille de chiffres est illisible en vocal. À ne pas confondre avec le tableau de mise en page — utiliser<table>pour positionner des éléments est un anti-pattern : la mise en page, c’est CSS grid/flexbox.
🎯 Objectifs
- Construire un tableau de données accessible :
<caption>,<thead>/<tbody>,<th scope>. - Comprendre comment
scopeassocie en-têtes et cellules pour le lecteur d’écran. - Traiter les cas complexes avec
headers/id. - Distinguer tableau de données (légitime) et tableau de mise en page (à bannir).
L’anatomie d’un tableau accessible
Un tableau de données présente des valeurs organisées par lignes et colonnes, où chaque cellule ne prend son sens que par ses en-têtes. Les briques :
<table>— le conteneur.<caption>— le titre du tableau, premier enfant de<table>. Il donne le sujet (« Progression des apprenants »), annoncé par le lecteur d’écran quand on entre dans le tableau.<thead>/<tbody>(et<tfoot>) — séparent la ligne d’en-tête du corps des données.<th>— une cellule d’en-tête (contre<td>pour une cellule de donnée). C’est<th>, pas un<td>en gras, qui déclare « ceci est un en-tête ».scope— sur chaque<th>, précise s’il coiffe une colonne (scope="col") ou une ligne (scope="row").
<!-- ✅ Tableau de données complet et accessible -->
<table>
<caption>Progression des apprenants — module React</caption>
<thead>
<tr>
<th scope="col">Apprenant</th>
<th scope="col">Leçons faites</th>
<th scope="col">Score</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Camille</th>
<td>8 / 10</td>
<td>92 %</td>
</tr>
<tr>
<th scope="row">Idris</th>
<td>5 / 10</td>
<td>78 %</td>
</tr>
</tbody>
</table>Ce que scope change pour le lecteur d’écran
C’est ici que tout se joue. Quand scope est correctement posé, le lecteur d’écran associe chaque cellule à ses en-têtes et les rappelle au fil de la navigation. En parcourant la cellule « 92 % », l’utilisateur entend quelque chose comme « Camille, Score, 92 % » — l’en-tête de ligne et l’en-tête de colonne, replacés dans leur contexte.
Sans <th>/scope (tableau tout en <td>), le lecteur d’écran énonce des valeurs nues : « 92 % », « 78 % », « 8 sur 10 »… sans jamais dire de quoi ni de qui il s’agit. En navigation vocale, où l’on ne voit pas la grille d’un coup d’œil, cela rend le tableau inexploitable : l’utilisateur perd le fil dès la deuxième cellule.
<!-- ❌ Tout en <td>, pas de caption, pas de scope : valeurs orphelines -->
<table>
<tr>
<td><strong>Apprenant</strong></td>
<td><strong>Score</strong></td>
</tr>
<tr>
<td>Camille</td>
<td>92 %</td>
</tr>
</table>Le gras du premier rang est purement visuel : rien ne dit au lecteur d’écran que ce sont des en-têtes. Il faut de vrais <th scope>.
♿ Côté utilisateur — Sur un tableau bien balisé, l’utilisateur navigue de cellule en cellule (touches de navigation dans les tableaux) et entend à chaque fois l’en-tête pertinent : « Idris, Leçons faites, 5 sur 10 ». Il peut aussi sauter directement à une colonne. Sur un tableau tout en
<td>, il entend « 5 sur 10 » sans contexte, doit mémoriser la position, recompter les colonnes de tête… un effort épuisant pour une information quescopelivrerait gratuitement.
Cas complexes : headers et id
Quand un tableau a des en-têtes sur plusieurs niveaux (en-têtes fusionnés, doubles en-têtes), scope ne suffit plus à lever l’ambiguïté. On associe alors explicitement chaque cellule à ses en-têtes en donnant un id à chaque <th> et en listant ces id dans l’attribut headers de la cellule.
<!-- ✅ Association explicite pour un tableau à en-têtes complexes -->
<table>
<caption>Créneaux par formateur</caption>
<tr>
<th id="form">Formateur</th>
<th id="lun">Lundi</th>
<th id="mar">Mardi</th>
</tr>
<tr>
<th id="sofia" headers="form">Sofia</th>
<td headers="sofia lun">9 h – 11 h</td>
<td headers="sofia mar">14 h – 16 h</td>
</tr>
</table>Garde headers/id pour les vrais cas complexes : sur un tableau simple, scope est plus lisible et suffit largement. La bonne règle : commence par scope, ne passe à headers/id que si la structure l’exige.
💡 Réflexe — Avant de coder un tableau, demande-toi : quelle cellule est un en-tête ? Le premier rang, souvent, mais aussi parfois la première colonne (les noms des apprenants). Ces cellules-là sont des
<th scope="col">ou<th scope="row">, jamais des<td>en gras. Poser mentalement les en-têtes avant d’écrire le HTML évite le tableau tout-<td>qui ne s’annonce pas.
Données vs mise en page : ne pas confondre
Historiquement, avant CSS, on détournait <table> pour positionner des blocs (colonnes, gabarits d’e-mail). C’est aujourd’hui un anti-pattern sur le web : un tableau de mise en page injecte une sémantique de tableau fausse (« tableau, 3 colonnes, 2 lignes ») là où il n’y a aucune donnée tabulaire, ce qui parasite le lecteur d’écran avec des annonces de lignes et colonnes dénuées de sens.
La règle est nette :
- Des données organisées en lignes/colonnes, où les cellules dépendent d’en-têtes (notes, plannings, comparatifs) →
<table>, c’est fait pour ça. - Un agencement visuel (mettre deux blocs côte à côte, faire une grille de cartes) → CSS, avec
display: gridoudisplay: flex. Aucun<table>.
<!-- ❌ Tableau de mise en page : sémantique de tableau parasite -->
<table>
<tr>
<td><img src="/logo.svg" alt="A11yLearn" /></td>
<td><nav>…</nav></td>
</tr>
</table>
<!-- ✅ Mise en page en CSS : sémantique honnête, pas de faux tableau -->
<header class="site-header">
<img src="/logo.svg" alt="A11yLearn" />
<nav aria-label="Principale">…</nav>
</header>.site-header { display: flex; align-items: center; gap: 1rem; }⚠️ Piège — « Ça s’aligne bien en
<table>, je garde. » Non : si les cellules ne sont pas des données liées à des en-têtes, le<table>ment au lecteur d’écran, qui annoncera un tableau et son nombre de lignes/colonnes pour rien. Réserve<table>aux données ; pour disposer des éléments à l’écran, c’est grid/flexbox, point.
🧭 Sur A11yLearn — Le tableau de progression des apprenants d’A11yLearn était une grille de
<div>alignées en CSS grid : visuellement parfaite, mais au lecteur d’écran, juste une avalanche de chiffres (« 92 », « 78 », « 8 ») sans nom ni contexte. On le reconstruit en vrai<table>: un<caption>« Progression des apprenants », des<th scope="col">pour Leçons/Score, un<th scope="row">pour chaque nom d’apprenant. Désormais l’utilisateur de NVDA entend « Idris, Score, 78 % » cellule après cellule et peut auditer la progression de sa classe. Les données méritaient un tableau — le voilà, honnête et navigable.
📚 Aller plus loin — L’association en-têtes/cellules relève du critère 1.3.1 (Information et relations) : la relation qu’un lecteur voyant perçoit visuellement (cette valeur est dans la colonne « Score », ligne « Idris ») doit exister dans le code via
<th>,scopeouheaders/id. Le<caption>sert aussi le critère 2.4.6 (En-têtes et étiquettes) en nommant le tableau. La référence détaillée : le tutoriel « Tables » du W3C WAI.
✏️ Exercices
Exercice 1 — Balise correctement. Transforme cette grille tout-<td> en tableau de données accessible : colonnes « Cours », « Durée », « Niveau » ; une ligne « React accessible / 4 h / Avancé ».
✅ Solution
<table>
<caption>Catalogue des cours</caption>
<thead>
<tr>
<th scope="col">Cours</th>
<th scope="col">Durée</th>
<th scope="col">Niveau</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">React accessible</th>
<td>4 h</td>
<td>Avancé</td>
</tr>
</tbody>
</table>Un <caption> nomme le tableau, la ligne d’en-tête est en <th scope="col">, et le nom du cours (en-tête de sa ligne) en <th scope="row">.
Exercice 2 — Données ou mise en page ? Pour chacun, dis s’il faut un <table> ou du CSS : (a) un comparatif de trois formules d’abonnement avec leurs prix et fonctionnalités ; (b) une page d’accueil avec une image à gauche et un texte à droite.
✅ Solution
(a) <table> — ce sont des données comparatives (formules × critères), avec des en-têtes de colonnes et de lignes ; le tableau est légitime et attendu. (b) CSS (display: flex ou grid) — c’est un simple agencement visuel, sans données tabulaires ; un <table> y injecterait une fausse sémantique. Le critère : y a-t-il des données liées à des en-têtes, ou juste un placement à l’écran ?
🧠 Quiz de révision
1. À quoi sert <caption> ?
<caption> ?À nommer le tableau. Premier enfant de <table>, il annonce le sujet (« Progression des apprenants ») au lecteur d’écran dès qu’on entre dans le tableau, donnant le contexte global. Il sert le critère 2.4.6 (En-têtes et étiquettes).
2. Que fait l’attribut scope sur un <th> ?
scope sur un <th> ?Il déclare si l’en-tête coiffe une colonne (scope="col") ou une ligne (scope="row"). Cela permet au lecteur d’écran d’associer chaque cellule à ses en-têtes et de les rappeler (« Camille, Score, 92 % ») pendant la navigation.
3. Pourquoi un <td> en gras ne remplace-t-il pas un <th> ?
<td> en gras ne remplace-t-il pas un <th> ?Parce que le gras est purement visuel : rien dans le code ne dit que la cellule est un en-tête. Seul <th> (avec scope) déclare la relation en-tête/cellule que le lecteur d’écran exploite. Sans lui, les valeurs sont annoncées sans contexte.
4. Quand utiliser headers/id plutôt que scope ?
headers/id plutôt que scope ?Pour les tableaux à en-têtes complexes (plusieurs niveaux, en-têtes fusionnés) où scope ne lève pas l’ambiguïté. On donne un id à chaque <th> et on liste ces id dans l’attribut headers des cellules. Sur un tableau simple, scope suffit et reste plus lisible.
5. Pourquoi ne pas utiliser <table> pour la mise en page ?
<table> pour la mise en page ?Parce qu’il injecte une sémantique de tableau fausse (annonces de lignes/colonnes) là où il n’y a pas de données, ce qui parasite le lecteur d’écran. La disposition visuelle relève de CSS grid/flexbox ; <table> est réservé aux vraies données tabulaires.
Partie suivante : Clavier & gestion du focus — maintenant que les fondations sémantiques sont posées, on rend tout utilisable au clavier et on maîtrise le focus.