Les onglets
Rien en HTML ne fait un système d'onglets : tout est à déclarer. Et le piège n'est pas les rôles, c'est le clavier — la tabulation ne circule pas entre les onglets. Démo, code, critères RGAA.
4 critères RGAA en jeu
Essayez-les
Le moteur passe soixante-deux règles ACT sur la page et rattache chaque relevé à son critère. Il ne prononce jamais de verdict : il apporte une preuve.
Quatorze assistants conduisent les vérifications que rien n'automatise — les images, les liens, les tableaux, les formulaires — une question à la fois.
Le taux de conformité, les critères tranchés, les preuves attachées : l'audit s'exporte tel qu'il se relira dans six mois.
Posez le focus sur un onglet, puis n’utilisez que le clavier : les flèches changent d’onglet, Tab sort du groupe et va au panneau. C’est l’inverse de ce qu’on attend, et c’est exactement le point de ce composant.
Cette démo existe aussi en page autonome.
La règle qu’on ignore presque toujours
Un groupe d’onglets est un seul arrêt de tabulation, pas un par onglet.
La raison est arithmétique. Avec six onglets tabulables, atteindre le contenu
coûte six pressions — et il faut recommencer à chaque visite. Le motif retenu
partout est donc le tabindex roulant : l’onglet choisi porte tabindex="0",
tous les autres tabindex="-1", et les flèches déplacent à la fois le choix et
le focus.
C’est le même principe que dans un groupe de boutons radio, et pour la même raison : quand des éléments forment un choix, on tabule jusqu’au groupe, puis on choisit dedans.
L’erreur courante
Des liens d’ancre stylés en onglets, ou des div cliquables.
<!-- À ne pas faire : rien n'annonce un système d'onglets -->
<div class="onglets">
<div class="onglet actif" onclick="afficher(0)">Détection</div>
<div class="onglet" onclick="afficher(1)">Assistants</div>
</div>
<div class="panneau">…</div> Ce qui manque : la nature du composant (personne ne sait que c’est un groupe
d’onglets), l’état choisi (la classe actif ne parle qu’au CSS), le nombre
(« onglet 1 sur 3 » n’est pas annoncé), le lien onglet/panneau, et tout le
clavier.
Et si l’on remplace les div par des <button> sans rôles ? Le clavier
fonctionne, mais chaque onglet devient un arrêt de tabulation, et un lecteur
d’écran annonce trois boutons sans rapport les uns avec les autres. Mieux, mais
toujours pas des onglets.
Le code, pas à pas
1. Les trois rôles, et le nom du groupe
<div role="tablist" aria-label="Ce que fait l'extension">
<button
type="button"
role="tab"
id="onglet-detection"
aria-selected="true"
aria-controls="panneau-detection"
tabindex="0"
>
Détection
</button>
…
</div>
<div
role="tabpanel"
id="panneau-detection"
aria-labelledby="onglet-detection"
tabindex="0"
>
…
</div> role="tablist" déclare le groupe, role="tab" chaque onglet, role="tabpanel" chaque panneau. aria-selected porte l’état, aria-controls et aria-labelledby nouent l’onglet et son panneau dans les deux sens.
L’aria-label du groupe compte : sans lui, un lecteur d’écran annonce « liste
d’onglets, onglet 1 sur 3 » sans dire de quoi.
Le tabindex="0" sur le panneau n’est pas une erreur : il permet d’y entrer
au clavier. Sans lui, un panneau qui défile serait inatteignable — c’est une
zone défilante hors de l’ordre de tabulation, et notre moteur la relève.
2. Le tabindex roulant
onglets.forEach((onglet, rang) => {
onglet.setAttribute('aria-selected', String(rang === choisi));
onglet.tabIndex = rang === choisi ? 0 : -1;
panneaux[rang].hidden = rang !== choisi;
}); Trois attributs qui bougent ensemble, et qui doivent le rester : un aria-selected figé pendant que la classe CSS change est le défaut le plus
courant de ce composant.
3. Les flèches
tablist.addEventListener('keydown', (evenement) => {
const cibles = {
ArrowRight: choisi + 1,
ArrowLeft: choisi - 1,
Home: 0,
End: onglets.length - 1,
};
const cible = cibles[evenement.key];
if (cible === undefined) return;
evenement.preventDefault();
choisir((cible + onglets.length) % onglets.length);
onglets[choisi].focus();
}); Le preventDefault() est nécessaire : sans lui, la flèche change d’onglet et fait défiler la page. Le modulo donne le bouclage, attendu du dernier
onglet vers le premier.
Et surtout : focus() accompagne le choix. Changer d’onglet sans déplacer le
focus laisse l’utilisateur au clavier sur un onglet qui n’est plus le bon.
4. Choisir ouvre, ou faut-il valider ?
Deux comportements existent. L’activation automatique — la flèche ouvre aussitôt le panneau — est le bon défaut : elle laisse parcourir les contenus sans valider à chaque fois. C’est ce que fait la démo.
L’activation manuelle — la flèche déplace, Entrée ou
Espace ouvre — ne se justifie que si l'ouverture coûte cher : un chargement réseau, un rendu lourd. Sinon elle ajoute une pression pour rien.5. Avant de coder ça : en avez-vous besoin ?
C’est le composant le plus coûteux de cette collection, et le plus facile à rater. Trois questions valent d’être posées avant :
- Les contenus sont-ils exclusifs ? S’ils se lisent à la suite, des titres et des sections font mieux, et se cherchent au navigateur.
- Faut-il pouvoir lier un contenu précis ? Des onglets ne sont pas dans l’URL ; des sections ancrées le sont.
- Faut-il pouvoir imprimer ou chercher dans la page ? Les panneaux masqués échappent aux deux.
Un empilement de sections titrées est souvent la meilleure réponse — et il ne demande aucun ARIA.
Au clavier
| Touche | Effet |
|---|---|
| Tab | Entre dans le groupe (un seul arrêt), puis en sort vers le panneau. |
| ← → | Change d’onglet, avec bouclage du dernier au premier. |
| Début / Fin | Va au premier ou au dernier onglet. |
| Entrée / Espace | Ouvre l’onglet — utile seulement en activation manuelle. |
Ce que le lecteur d’écran restitue
Sur un onglet : le nom du groupe, la nature, le rang et l’état — de l’ordre de « Ce que fait l’extension, liste d’onglets, Détection, onglet, sélectionné, 1 sur 3 ». En entrant dans le panneau : son nom, hérité de l’onglet qui le gouverne.
Si vous entendez « bouton » au lieu d’« onglet », les rôles manquent. Si vous
n’entendez pas « 1 sur 3 », le role="tablist" n’enveloppe pas les onglets. Si
la tabulation s’arrête sur chacun, le tabindex roulant n’est pas posé.
Checklist
role="tablist"nommé,role="tab",role="tabpanel"— les trois, ou aucun.aria-selected,tabindexethiddenbougent ensemble.- Un seul arrêt de tabulation pour tout le groupe.
- Flèches, Début et Fin, avec
preventDefault()etfocus(). tabindex="0"sur le panneau, pour qu’on puisse y entrer.- Demandez-vous d’abord si des sections titrées ne feraient pas mieux.
Les critères RGAA en jeu
- Critère 7.1 — Compatibilité des scripts
Aucun élément HTML ne fait un onglet : les rôles tablist, tab et tabpanel, et l'état aria-selected, sont la seule façon d'exposer le composant aux technologies d'assistance.
- Critère 7.3 — Contrôle des scripts au clavier
Choisir un onglet doit se faire au clavier — et par les flèches, pas par la tabulation, qui est réservée à la sortie du groupe.
- Critère 12.8 — Cohérence de l’ordre de tabulation
Un seul onglet entre dans l'ordre de tabulation : le tabindex roulant évite qu'un groupe de six onglets coûte six tabulations avant d'atteindre le contenu.
- Critère 10.8 — Contenus cachés ignorés
Les panneaux non choisis sont masqués pour tout le monde. Laissés dans le flux, leurs liens restent focalisables et le clavier tombe dans un contenu invisible.
Vérifiez votre composant avec l'extension
Pour aller plus loin
- L’accordéon : le même besoin de plier du contenu, sans aucun ARIA à inventer.
- Critère 12.8 — Cohérence de l’ordre de tabulation : ce que le tabindex roulant protège.
- Le motif Tabs de l’ARIA APG : la référence de comportement dont ce guide est l’application.
Sources
- ARIA APG — Tabs Pattern — le contrat de comportement, tabindex roulant compris
- MDN — ARIA
tabrole — la référence des trois rôles - WCAG — Understanding 2.1.1 Keyboard — l’exigence de fonctionnement au clavier
- GOV.UK Design System — Tabs — quand des onglets sont le bon choix