Onglets accessibles (tabs) : role tablist, roving tabindex
Le contrat du motif tabs : rôles tablist, tab et tabpanel, aria-selected toujours synchronisé avec tabindex, un seul arrêt de tabulation et les critères RGAA à vérifier.
Un système d'onglets est un ensemble de panneaux superposés dont un seul est visible à la fois, chaque onglet affichant le panneau qui lui correspond. Contrairement au contenu dépliable ou à la modale, aucun élément HTML natif ne couvre ce motif : les rôles (tablist, tab, tabpanel), les états, le roving tabindex et tout le clavier sont à votre charge, sans filet. Cette fiche est le contrat de référence du motif ; pour le pas-à-pas d'implémentation complet : Créer des onglets accessibles : rôles ARIA et navigation clavier.
Quand ce motif est le mauvais choix
- L'activation charge une autre URL : ce sont des liens de navigation, pas des onglets. Un
role="tab"posé sur un<a href>annonce un onglet qui se comporte en lien ; Entrée navigue au lieu de changer de panneau. - Les sections gagnent à être lues à la suite, ou trouvées via le plan de titres : un accordéon, voire de simples titres, sert mieux le contenu.
- Une seule zone à afficher ou masquer : un contenu dépliable (bouton +
aria-expanded) est plus simple et beaucoup plus difficile à rater. - Le choix fixe la valeur d'un champ de formulaire : liste déroulante ou groupe de boutons radio, pas des onglets.
Critères RGAA applicables
- Critère 7.1 : compatibilité des scripts avec les technologies d'assistance. Les rôles
tablist,tab,tabpanelet l'étataria-selecteddoivent être exposés : aucun élément natif ne les fournit à votre place. - Critère 7.3 : script contrôlable au clavier et par tout dispositif de pointage. Les flèches déplacent le focus d'onglet en onglet, et le composant entier reste utilisable à la souris comme au clavier.
- Critère 10.7 : prise de focus visible. L'onglet focalisé doit montrer un indicateur visible, distinct du style de l'onglet sélectionné : en activation manuelle, focus et sélection vivent sur deux onglets différents au même moment.
- Critère 10.8 : contenus cachés ignorés des technologies d'assistance. Les panneaux inactifs se masquent avec
hiddenoudisplay: none, jamais en les recouvrant visuellement. - Critère 12.8 : ordre de tabulation cohérent. Le roving tabindex fait de toute la barre d'onglets un seul arrêt de tabulation, qui atterrit sur l'onglet sélectionné.
- Critère 12.9 : absence de piège au clavier. Tab doit toujours sortir de la barre vers le panneau ; capturer Tab pour circuler entre les onglets fabrique un piège.
Interaction clavier attendue
| Touche | Action |
|---|---|
| Tab | Entre dans la barre sur l'onglet sélectionné ; l'appui suivant sort vers le panneau |
| Flèche droite / Flèche gauche | Onglet suivant / précédent (barre horizontale), avec bouclage |
| Flèche bas / Flèche haut | Onglet suivant / précédent quand la barre est verticale (aria-orientation="vertical") |
| Entrée / Espace | Active l'onglet focalisé, si l'activation n'est pas automatique au focus |
| Début / Fin | (Optionnel) premier / dernier onglet |
Deux stratégies d'activation existent. Automatique : la sélection suit le focus, adaptée quand les panneaux s'affichent instantanément ; c'est le choix par défaut du WAI-ARIA APG. Manuelle : les flèches déplacent seulement le focus, Entrée ou Espace sélectionne ; à préférer quand chaque changement de panneau est coûteux, destructif ou déclenche une requête (traverser cinq onglets aux flèches ne doit pas lancer cinq appels réseau).
Rôles et attributs ARIA
role="tablist" sur le conteneur, nommé par aria-label dès que la page peut contenir plusieurs barres. role="tab" sur chaque onglet, qui doit être un vrai <button>. role="tabpanel" sur chaque panneau, relié à son onglet par aria-labelledby. Exactement un onglet porte aria-selected="true", et le roving tabindex (sans être un attribut ARIA) est indissociable du motif : tabindex="0" sur l'onglet sélectionné, tabindex="-1" sur tous les autres, toujours synchronisé avec aria-selected. Un panneau sans élément focalisable reçoit tabindex="0" pour rester atteignable et défilable au clavier ; un panneau qui contient déjà un lien ou un bouton n'en a pas besoin.
<!-- Nommer la tablist : plusieurs barres d'onglets sur une page sont
sinon indiscernables au lecteur d'écran. -->
<div role="tablist" aria-label="Paramètres du compte">
<!-- Onglet sélectionné : aria-selected="true" ET tabindex="0", toujours ensemble. -->
<button type="button" role="tab" id="onglet-profil"
aria-selected="true" aria-controls="panneau-profil" tabindex="0">Profil</button>
<button type="button" role="tab" id="onglet-securite"
aria-selected="false" aria-controls="panneau-securite" tabindex="-1">Sécurité</button>
</div>
<!-- tabindex="0" sur le panneau : sans élément focalisable à l'intérieur,
il serait sinon inatteignable et indéfilable au clavier. -->
<div role="tabpanel" id="panneau-profil" aria-labelledby="onglet-profil" tabindex="0">
<p>Paramètres du profil.</p>
</div>
<div role="tabpanel" id="panneau-securite" aria-labelledby="onglet-securite" tabindex="0" hidden>
<p>Paramètres de sécurité.</p>
</div>
const barre = document.querySelector('[role="tablist"]');
const onglets = [...barre.querySelectorAll('[role="tab"]')];
function selectionner(nouvelOnglet) {
for (const onglet of onglets) {
const estSelectionne = onglet === nouvelOnglet;
// aria-selected et tabindex bougent ENSEMBLE. S'ils divergent, Tab ramène
// l'utilisateur sur un onglet qui n'est pas celui du panneau affiché.
onglet.setAttribute('aria-selected', String(estSelectionne));
onglet.setAttribute('tabindex', estSelectionne ? '0' : '-1');
document.getElementById(onglet.getAttribute('aria-controls')).hidden = !estSelectionne;
}
// aria-selected ne déplace PAS le focus. Cet appel, si.
nouvelOnglet.focus();
}
barre.addEventListener('keydown', (evenement) => {
const indice = onglets.indexOf(evenement.target);
if (indice === -1) return;
let suivant = null;
if (evenement.key === 'ArrowRight') suivant = (indice + 1) % onglets.length;
if (evenement.key === 'ArrowLeft') suivant = (indice - 1 + onglets.length) % onglets.length;
if (evenement.key === 'Home') suivant = 0;
if (evenement.key === 'End') suivant = onglets.length - 1;
if (suivant === null) return; // Tab, Échap et le reste passent sans interception
evenement.preventDefault(); // uniquement pour les touches réellement gérées
selectionner(onglets[suivant]);
});
Côté styles, sélection et focus sont deux états différents et doivent rester visuellement distincts : [aria-selected="true"] marque l'onglet ouvert, :focus-visible marque l'onglet focalisé. Un anneau de focus qui n'apparaît que sur l'onglet sélectionné rend la navigation aux flèches invisible en activation manuelle.
À ne pas faire :
/* À ne pas faire : masquer les panneaux inactifs visuellement.
Tous les panneaux restent dans l'arbre d'accessibilité et l'ordre de
tabulation ; le lecteur d'écran restitue l'ensemble comme un seul mur
de contenu. Utiliser l'attribut hidden. */
[role="tabpanel"][data-inactif] {
opacity: 0;
position: absolute;
z-index: -1;
}
Défauts fréquents et impact utilisateur
- Chaque onglet dans l'ordre de tabulation (pas de roving tabindex) : une barre de neuf onglets coûte neuf appuis Tab avant d'atteindre le contenu.
aria-selectedmodifié sans appel à.focus(): le bug le plus courant du motif. Le focus reste sur l'ancien onglet ; au lecteur d'écran, les flèches semblent ne rien faire puisque rien n'est annoncé.aria-selectedettabindexdésynchronisés : Tab ramène sur un onglet qui n'est pas celui du panneau affiché ; l'utilisateur repart systématiquement du mauvais endroit.<div role="tab">au lieu de<button>: non focalisable, Entrée et Espace inopérants ; la barre d'onglets n'existe tout simplement pas au clavier.- Panneaux inactifs masqués en
opacityouz-index: tout le contenu reste restitué et tabulable en même temps. preventDefaultglobal dans le gestionnaire clavier : Tab (la seule sortie), Échap et les touches de lecture du lecteur d'écran sont neutralisés d'un coup.- Barre sans nom quand la page en contient plusieurs : toutes annoncées « liste d'onglets », impossible de les distinguer.
Ce que les outils automatiques ne détectent pas
Un scanner automatique, le nôtre compris, repère un role="tab" orphelin hors de toute tablist (contexte ARIA requis) ou un aria-controls pointant vers un identifiant absent. Mais les trois défauts qui partent réellement en production lui échappent :
- le roving tabindex manquant : neuf onglets, neuf arrêts de tabulation, et un balisage ARIA pourtant valide ; seul un parcours clavier le révèle ;
- le focus qui ne suit pas la sélection : c'est un comportement, pas un attribut ; il faut écouter ce qui est annoncé aux flèches ;
- les panneaux masqués seulement visuellement : l'affichage est correct, seule une lecture au lecteur d'écran révèle le mur de contenu.
Vérifier ce composant
Protocole manuel, trois minutes :
- Le test du roving tabindex : sélectionnez le troisième onglet, sortez de la barre, revenez par Tab. Vous devez atterrir sur le troisième onglet, pas sur le premier. Sinon,
aria-selectedettabindexont divergé. - Le test de la sortie : depuis un onglet, appuyez sur Tab. Vous devez arriver dans le panneau (ou après lui), jamais sur l'onglet suivant.
- Au lecteur d'écran : attendez-vous à « libellé, onglet, sélectionné, 1 sur 3 », et à une annonce à chaque flèche. Si les flèches restent muettes,
aria-selectedchange sans.focus().
Deux outils gratuits du site pour appuyer la vérification : le testeur de focus visible valide que vos styles :focus-visible restent perceptibles sur l'onglet focalisé, distinct de l'onglet sélectionné ; le simulateur de lecteur d'écran affiche l'arbre d'accessibilité, où la structure tablist / tab / tabpanel et l'état aria-selected se lisent directement. La méthode de parcours complète est détaillée dans le guide Tester au clavier.
Approfondir avec les fiches critères
Vérifier ce composant sur votre site ?
Le scan repère les défauts détectables automatiquement (rôles incohérents, champs sans étiquette, attributs ARIA orphelins) ; le reste se vérifie à la main avec les protocoles de cette fiche.
Lancer un scan gratuit