Composant · Afficher et masquer

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

Interaction clavier attendue

ToucheAction
TabEntre dans la barre sur l'onglet sélectionné ; l'appui suivant sort vers le panneau
Flèche droite / Flèche gaucheOnglet suivant / précédent (barre horizontale), avec bouclage
Flèche bas / Flèche hautOnglet suivant / précédent quand la barre est verticale (aria-orientation="vertical")
Entrée / EspaceActive 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-selected modifié 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-selected et tabindex dé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 opacity ou z-index : tout le contenu reste restitué et tabulable en même temps.
  • preventDefault global 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 :

  1. 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-selected et tabindex ont divergé.
  2. 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.
  3. 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-selected change 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

Toutes les fiches composants accessibles