RGAA Lab

Composants accessibles

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.

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

ToucheEffet
TabEntre dans le groupe (un seul arrêt), puis en sort vers le panneau.
Change d’onglet, avec bouclage du dernier au premier.
Début / FinVa au premier ou au dernier onglet.
Entrée / EspaceOuvre 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, tabindex et hidden bougent ensemble.
  • Un seul arrêt de tabulation pour tout le groupe.
  • Flèches, Début et Fin, avec preventDefault() et focus().
  • 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

Vérifiez votre composant avec l'extension

Pour aller plus loin

Sources