Le tableau complexe
Un tableau à deux niveaux d'en-têtes se lit à l'aveugle dans une seule direction : celle que le code lui donne. Le tenir demande quatre règles ; le scinder ou l'aplatir n'en demande aucune. Démo, code, critères RGAA.
4 critères RGAA en jeu
Essayez-les
1. Le tableau complexe, tenu
| Accueil | Contact | |||
|---|---|---|---|---|
| Non conformes | Conformes | Non conformes | Conformes | |
| Images | 2 | 7 | 0 | 9 |
| Liens | 1 | 4 | 1 | 4 |
2. Scindé : un tableau simple par page
| Non conformes | Conformes | |
|---|---|---|
| Images | 2 | 7 |
| Liens | 1 | 4 |
| Non conformes | Conformes | |
|---|---|---|
| Images | 0 | 9 |
| Liens | 1 | 4 |
3. Aplati : la page devient une colonne
| Page | Thématique | Non conformes | Conformes |
|---|---|---|---|
| Accueil | Images | 2 | 7 |
| Accueil | Liens | 1 | 4 |
| Contact | Images | 0 | 9 |
| Contact | Liens | 1 | 4 |
Les trois grilles portent la même donnée. Ouvrez le mode tableau de votre lecteur d’écran et parcourez la première cellule par cellule : chaque valeur doit s’annoncer avec trois en-têtes — la thématique, la page, puis la colonne — « Images, Accueil, Non conformes, 2 ». Passez ensuite aux deux suivantes : deux en-têtes suffisent, et rien ne manque. C’est tout le propos de la fiche.
Cette démo existe aussi en page autonome.
L’erreur courante
L’en-tête à deux niveaux déclaré avec scope="colgroup", et une cellule
d’en-tête vide dans le coin.
<!-- À ne pas faire : le scope ne couvre pas une colonne entière, et le coin est un th -->
<table>
<tr>
<th rowspan="2"></th>
<th colspan="2" scope="colgroup">Accueil</th>
<th colspan="2" scope="colgroup">Contact</th>
</tr>
<tr>
<th scope="col">Non conformes</th>
<th scope="col">Conformes</th>
<th scope="col">Non conformes</th>
<th scope="col">Conformes</th>
</tr>
<tr>
<th scope="row">Images</th>
<td>2</td><td>7</td><td>0</td><td>9</td>
</tr>
</table> C’est le code que les guides WCAG enseignent, et il n’est pas absurde : colgroup existe pour cela. Mais le RGAA lit autrement. Son test 5.7.3 dit
qu’un <th> qui ne s’applique pas à la totalité d’une ligne ou d’une
colonne ne porte pas d’attribut scope, et porte un id unique. « Accueil »
chapeaute deux colonnes sans en couvrir aucune en entier : il est dans ce cas.
Un audit RGAA marque donc ce tableau non conforme au 5.7, tout scope qu’il ait.
Le <th> vide du coin est une erreur plus discrète. Sur Safari et Chrome avec
VoiceOver, cette cellule d’en-tête sans contenu entre dans les associations et
les brouille : des cellules s’annoncent avec un en-tête muet, ou avec le
mauvais. Le coin est une case vide, donc une donnée — un <td>.
Enfin, ce tableau fusionne dans les deux sens à la fois : un rowspan dans
le coin, des colspan sur la première ligne. C’est la combinaison que
VoiceOver restitue le plus mal, en associant des cellules à des en-têtes qui
ne sont ni sur leur ligne ni sur leur colonne.
Comment le coder
1. Décider s’il est complexe
Le glossaire du référentiel tranche : un tableau est complexe quand ses
en-têtes ne tiennent pas sur la seule première ligne et la seule première
colonne, ou quand un en-tête ne s’applique qu’à une partie de sa ligne ou de
sa colonne. Deux niveaux d’en-têtes de colonne, c’est complexe. Une double
entrée avec un seul niveau de chaque côté ne l’est pas — scope y suffit, et
la fiche tableaux de données s’en occupe.
Des cellules de données fusionnées ne rendent pas un tableau complexe. Elles posent un autre problème, traité au point 6.
2. Un seul axe de fusion
Si les en-têtes de colonne ont deux niveaux, les en-têtes de ligne n’en ont qu’un. Si les lignes sont regroupées, les colonnes ne le sont pas. Tenir les deux à la fois est possible en HTML et illisible en pratique : c’est le cas que la matrice de tests de l’article cité en source a vu échouer sur le plus de lecteurs.
<thead>
<tr>
<td rowspan="2"></td>
<th colspan="2" id="p-accueil">Accueil</th>
<th colspan="2" id="p-contact">Contact</th>
</tr>
<tr>
<th id="a-nc">Non conformes</th>
<th id="a-ok">Conformes</th>
<th id="c-nc">Non conformes</th>
<th id="c-ok">Conformes</th>
</tr>
</thead> Le rowspan du coin ne compte pas comme une fusion d’en-tête : c’est un <td> vide qui occupe la place, et aucune cellule ne le désigne.
3. id sur chaque en-tête, headers sur chaque cellule
Sans scope, tout passe par headers. Chaque cellule de données liste les id de tous ses en-têtes, du plus général au plus précis : la thématique,
la page, la colonne.
<tr>
<th id="t-images">Images</th>
<td headers="t-images p-accueil a-nc">2</td>
<td headers="t-images p-accueil a-ok">7</td>
<td headers="t-images p-contact c-nc">0</td>
<td headers="t-images p-contact c-ok">9</td>
</tr> La technique se tient sur tout le tableau : dès qu’un en-tête est lié par id,
les en-têtes de ligne le sont aussi, sans scope="row". Mélanger les deux
techniques dans une grille donne des restitutions différentes selon le lecteur.
Un id doit être unique dans la page, pas seulement dans le tableau. Deux
tableaux qui reprennent p-accueil se volent leurs en-têtes.
4. Le coin en <td>
La cellule vide au croisement des en-têtes de ligne et de colonne est une
donnée absente, pas un en-tête. Elle reste un <td>, sans id, et rien ne la
désigne dans un headers.
5. Le résumé de structure
Le tableau complexe doit un résumé (critère 5.1). En HTML5, la seule
technique du référentiel qui tienne encore est de l’écrire dans la caption,
à la suite du titre — l’attribut summary est obsolète. Le résumé décrit comment lire : le nombre de niveaux, ce que chacun représente, ce qui n’y est
pas.
<caption>
<span id="titre-complexe">Critères par thématique et par page auditée</span>
<span class="resume">
Les en-têtes de colonne sont sur deux niveaux : la page auditée, puis
pour chacune le nombre de critères non conformes et conformes. Une ligne
par thématique ; pas de ligne de total.
</span>
</caption> Un résumé qui redit le titre, ou qui décrit les chiffres, ne dit rien de la structure : présent, il tombe au 5.2.
6. Scinder plutôt que fusionner
Avant d’écrire un seul headers, poser la question : ce tableau doit-il être
un ? Un tableau à deux niveaux de colonnes est presque toujours plusieurs tableaux simples juxtaposés — un par page, ici. Chacun se déclare avec scope, aucun ne doit de résumé, et un lecteur d’écran y navigue sans
surprise.
<table>
<caption>Page Accueil — critères par thématique</caption>
<thead>
<tr><td></td><th scope="col">Non conformes</th><th scope="col">Conformes</th></tr>
</thead>
<tbody>
<tr><th scope="row">Images</th><td>2</td><td>7</td></tr>
</tbody>
</table> Même raisonnement pour les tableaux stratifiés — ceux où une ligne d’en-tête fusionnée sur toute la largeur ouvre une section (« Trimestre 1 », puis « Trimestre 2 ») : une section est un tableau, ou la strate devient une colonne.
7. Aplatir : la strate devient une colonne
L’autre sortie garde un seul tableau et lui rend sa simplicité. Ce qui était un niveau d’en-tête devient une colonne de données ; chaque ligne porte alors sa page et sa thématique, et la grille n’a plus qu’un niveau de chaque côté.
<thead>
<tr>
<th scope="col">Page</th>
<th scope="col">Thématique</th>
<th scope="col">Non conformes</th>
<th scope="col">Conformes</th>
</tr>
</thead>
<tbody>
<tr><td>Accueil</td><td>Images</td><td>2</td><td>7</td></tr>
<tr><td>Contact</td><td>Images</td><td>0</td><td>9</td></tr>
</tbody> On y perd une chose : la valeur « Accueil » se répète. C’est le prix, et il est bas — la répétition se lit, la fusion se devine. Le même principe vaut pour les cellules de données fusionnées : la plupart des lecteurs d’écran les sautent en parcourant la ligne, et NVDA leur annonce le mauvais en-tête de ligne. Répéter la valeur est presque toujours préférable.
Au clavier
Un tableau complexe n’est pas interactif ; sa navigation appartient au lecteur d’écran. La différence avec un tableau simple tient à ce qui est annoncé à chaque déplacement, pas aux touches.
| Touche | Effet |
|---|---|
| Ctrl + Alt + flèches | Déplace de cellule en cellule (NVDA, JAWS) ; à chaque changement de colonne, les deux niveaux d’en-tête sont annoncés. |
| Ctrl + Alt + Début | Retourne à la première cellule du tableau — utile quand la lecture s’est perdue dans une fusion. |
| T | Va au tableau suivant (mode navigation rapide) ; sur la version scindée, c’est ainsi qu’on passe d’une page à l’autre. |
| Tab | Ne s’arrête que sur la zone de défilement, quand il y en a une, et sur les liens ou boutons que les cellules contiennent. |
Ce que le lecteur d’écran restitue
Sur le tableau tenu (la première grille), chaque cellule s’annonce avec ses
trois en-têtes : « Images, Accueil, Non conformes, 2 ». Sur les deux autres,
avec deux. Si un niveau manque à l’annonce, l’association est rompue quelque
part — un headers qui oublie un id, ou un scope là où il n’a plus cours.
La restitution varie beaucoup plus d’un lecteur à l’autre que sur un tableau simple, et c’est mesuré. Une matrice de tests publiée fin 2025 sur neuf couples navigateur/lecteur d’écran donne les limites du terrain : TalkBack n’a restitué les en-têtes de colonne dans aucune configuration ; VoiceOver sur iOS gère mal le second niveau ; sur Mac, il associe de travers dès que les fusions portent sur les deux axes ; NVDA parcourt les cellules de données fusionnées mieux que les autres, mais avec des en-têtes de ligne inattendus. La conclusion de l’auteur tient en une phrase : faire simple, ou prévoir de tester largement.
L’extension RGAA Lab relève ces pièges dans le code — coin en <th>, colonne
sans en-tête, fusions sur les deux axes, en-têtes de ligne répétés — et les
montre à côté de la question du critère 5.7, sans conclure à votre place. Le scope sur un en-tête fusionné, lui, est une non-conformité lue directement :
c’est le test 5.7.3.
Checklist
- Un seul axe de fusion : deux niveaux de colonnes ou des lignes regroupées, jamais les deux.
idunique sur chaque en-tête,headerscomplet sur chaque cellule, aucunscope— la technique se tient sur tout le tableau.- Le coin vide est un
<td>, sansid, désigné par personne. - Un résumé dans la
caption, qui décrit les niveaux et ce qui manque, pas le titre. - Des en-têtes de ligne uniques : deux lignes « Total » se confondent à l’oreille.
- Avant tout cela : scinder en tableaux simples, ou aplatir la strate en colonne, si la donnée le permet.
- Pas de cellule de données fusionnée quand répéter la valeur suffit.
Les critères RGAA en jeu
- Critère 5.1 — Résumé des tableaux complexes
Dès que les en-têtes ne tiennent plus sur la seule première ligne et la seule première colonne, le tableau est complexe au sens du glossaire et doit un résumé qui dit comment le lire.
- Critère 5.2 — Pertinence du résumé de tableau
Le résumé qui redit le titre, ou qui décrit le tableau d'à côté, est présent et inutile : il doit nommer les niveaux d'en-têtes et ce qu'il n'y a pas — une ligne de total, par exemple.
- Critère 5.6 — En-têtes de tableau déclarés
Les en-têtes de second niveau et les en-têtes de ligne sont des th ; la cellule vide du coin, elle, est une donnée et reste un td.
- Critère 5.7 — Association des cellules aux en-têtes
Un en-tête qui chapeaute deux colonnes ne couvre aucune colonne en entier : le test 5.7.3 lui interdit scope et lui impose un id unique, que chaque cellule désigne dans headers. Le scope="colgroup" des guides WCAG ne passe pas ici.
Vérifiez votre composant avec l'extension
Pour aller plus loin
- Les tableaux de données : le tableau
simple et la double entrée, là où
scopesuffit encore. - Critère 5.7 — Association des cellules aux en-têtes : le test 5.7.3 en entier, et ce que notre outil en lit.
- Critère 5.1 — Résumé des tableaux complexes : où passe la frontière, et ce qu’un résumé doit dire.
- Le glossaire — tableau de données complexe : la définition qui décide si vous êtes sur cette fiche ou sur l’autre.
Sources
- Guillaume Barbier — Des tableaux de données complexes accessibles, c’est possible — la matrice de tests sur neuf couples navigateur/lecteur, et les règles de terrain
- W3C WAI — Tables with Multi-Level Headers — la technique
id/headersen détail - Adrian Roselli — A Responsive Accessible Table — ce qui survit au petit écran
- MDN —
headers— la référence de l’attribut - RGAA 4.1.2 — critère 5.7 — les quatre tests, dont le 5.7.3