Skip to Content

Chapitre 10.3 — Menus, onglets, accordéon

⏱️ TL;DR — Quatre patterns qui reviennent tout le temps, du plus simple au plus câblé. La disclosure : un <button> avec aria-expanded qui montre/cache un bloc. Le menu button : un déclencheur aria-haspopup + aria-expanded qui ouvre un menu navigable aux flèches. Les onglets : role="tablist" / tab / tabpanel, sélection aux flèches avec roving tabindex et aria-selected. L’accordéon : une pile de boutons aria-expanded, chacun révélant sa section. Et le piège n°1 de ce chapitre : une barre de navigation stylée n’est pas un role="menu".

🎯 Objectifs

  • Coder une disclosure minimale et propre (aria-expanded).
  • Distinguer un vrai menu d’application d’une simple navigation.
  • Câbler des onglets avec roving tabindex, flèches et aria-selected.
  • Assembler un accordéon à partir de boutons aria-expanded.

Disclosure : le patron de base

La disclosure (« révélation ») est le pattern le plus simple, et la brique de presque tous les autres : un bouton qui montre ou cache un contenu. Le contrat tient en deux points — un vrai <button>, et un aria-expanded qui reflète l’état réel.

function Disclosure({ titre, children }) { const [ouvert, setOuvert] = useState(false); return ( <> <button type="button" aria-expanded={ouvert} aria-controls="section-faq" onClick={() => setOuvert(!ouvert)} > {titre} </button> <div id="section-faq" hidden={!ouvert}>{children}</div> </> ); }

Clavier attendu : Entrée/Espace basculent l’état — et comme c’est un vrai <button>, tu l’as gratuitement. Pas de flèches, pas de piège de focus. Note que l’élément natif <details>/<summary> fait exactement ça sans une ligne de JS ; ne code la version custom que si tu as besoin d’animer ou de contrôler l’état finement.

💡 Réflexe — Le seul état qui compte sur une disclosure, c’est aria-expanded. S’il reste bloqué à false alors que le contenu est visible (bug classique : on montre le bloc mais on oublie de mettre à jour l’attribut), le lecteur d’écran annonce « réduit » sur un contenu déplié. L’aria-expanded doit être piloté par le même état que la visibilité, jamais en parallèle.

C’est le piège de ce chapitre. Le rôle menu de l’APG désigne un menu d’application — comme le menu « Fichier / Édition » d’un logiciel : une liste d’actions qu’on parcourt aux flèches, où Tab fait sortir du menu. Ce n’est pas la même chose qu’une barre de navigation de site (des liens vers des pages).

Le réflexe faux consiste à coller role="menu" et role="menuitem" sur sa navigation principale « parce que ça s’appelle un menu ». En faisant ça, tu casses le comportement attendu : le lecteur d’écran annonce un menu d’actions, l’utilisateur s’attend aux flèches, et Tab cesse de parcourir les liens un à un.

<!-- ❌ Une navigation stylée en faux menu d'application : comportement cassé --> <ul role="menu"> <li role="menuitem"><a href="/cours">Cours</a></li> <li role="menuitem"><a href="/tarifs">Tarifs</a></li> </ul> <!-- ✅ Une navigation, c'est un landmark + une liste de liens. Point. --> <nav aria-label="Principale"> <ul> <li><a href="/cours">Cours</a></li> <li><a href="/tarifs">Tarifs</a></li> </ul> </nav>

Le vrai menu button, lui, s’utilise pour un menu d’actions (un bouton « ⋯ » qui ouvre Modifier / Dupliquer / Supprimer). Son déclencheur porte aria-haspopup="menu" et aria-expanded, et le menu se parcourt aux flèches.

<!-- Vrai menu d'actions : haspopup + expanded, navigation aux flèches --> <button type="button" aria-haspopup="menu" aria-expanded="false" aria-controls="menu-actions">Actions</button> <ul role="menu" id="menu-actions" hidden> <li role="menuitem" tabindex="-1">Modifier</li> <li role="menuitem" tabindex="-1">Dupliquer</li> <li role="menuitem" tabindex="-1">Supprimer</li> </ul>

Clavier attendu du menu button : Entrée/Espace/Flèche bas ouvrent le menu et placent le focus sur le premier menuitem ; Flèche haut/Flèche bas naviguent (roving tabindex) ; Échap ferme et rend le focus au bouton ; Tab ferme le menu.

⚠️ Piège — La barre de navigation d’un site n’est pas un role="menu". Le menu ARIA est réservé aux menus d’application (listes d’actions type « Fichier / Édition »), navigués aux flèches. Une nav de site, ce sont des liens : <nav> + <ul> + <a>. Poser role="menu" dessus casse la tabulation attendue et déroute les utilisateurs de lecteur d’écran, qui se retrouvent à chercher des flèches là où Tab suffisait.

Onglets : le pattern à roving tabindex

Les onglets regroupent trois rôles : tablist (le conteneur), tab (chaque onglet), tabpanel (chaque panneau). Un seul onglet est sélectionné (aria-selected="true") et un seul panneau visible. La navigation se fait aux flèches, avec un roving tabindex : un seul tab est dans la tabulation à la fois.

function Onglets({ items }) { const [actif, setActif] = useState(0); const onKey = (e) => { if (e.key === 'ArrowRight') setActif((actif + 1) % items.length); if (e.key === 'ArrowLeft') setActif((actif - 1 + items.length) % items.length); }; return ( <> <div role="tablist" aria-label="Réglages" onKeyDown={onKey}> {items.map((it, i) => ( <button key={i} role="tab" id={`tab-${i}`} aria-selected={i === actif} aria-controls={`panel-${i}`} tabIndex={i === actif ? 0 : -1} // roving tabindex > {it.titre} </button> ))} </div> {items.map((it, i) => ( <div key={i} role="tabpanel" id={`panel-${i}`} aria-labelledby={`tab-${i}`} hidden={i !== actif}> {it.contenu} </div> ))} </> ); }

Clavier attendu : Flèche gauche/droite changent d’onglet (et le sélectionnent, en mode « automatique »), Home/End vont au premier/dernier, Tab sort de la barre vers le panneau. Pense à appeler .focus() sur le nouvel onglet actif après chaque flèche — le tabIndex seul ne déplace pas le focus, il ne fait que redéfinir le point d’arrêt.

Accordéon : une pile de disclosures

L’accordéon n’est qu’un empilement de disclosures : chaque en-tête est un <button> avec aria-expanded, qui contrôle sa région. On enrobe généralement chaque bouton dans un titre (<h3>) pour que le lecteur d’écran puisse naviguer de section en section.

<h3> <button type="button" aria-expanded="true" aria-controls="sec-1"> Prérequis du cours </button> </h3> <div id="sec-1" role="region" aria-labelledby="…">…</div> <h3> <button type="button" aria-expanded="false" aria-controls="sec-2"> Programme détaillé </button> </h3> <div id="sec-2" role="region" hidden>…</div>

Contrairement aux onglets, l’accordéon n’exige pas de navigation aux flèches (les boutons sont de simples boutons dans le flux du Tab) ; l’APG les propose en option, mais Tab + Entrée/Espace suffisent. Plusieurs sections peuvent être ouvertes en même temps, ou une seule — c’est un choix de design, pas une règle d’accessibilité.

🧭 Sur A11yLearn — Le tableau de bord d’A11yLearn affichait ses filtres (« Niveau », « Durée », « Thème ») dans une barre stylée en <ul role="menu"> avec des <a> dedans. Au lecteur d’écran, c’était annoncé « menu » et les flèches étaient attendues — sauf qu’aucune n’était câblée. On a tranché selon la nature réelle : ces filtres naviguent vers des vues, donc ce sont des liens → une <nav aria-label="Filtres"> avec des <a>. Le vrai menu d’actions (le « ⋯ » sur chaque carte de cours : Reprendre / Marquer terminé / Retirer) est, lui, resté un vrai menu button avec aria-haspopup, navigation aux flèches et Échap. Deux composants, deux patterns — parce que l’un navigue et l’autre agit.

✏️ Exercices

Exercice 1 — Nav ou menu ? Pour chacun, dis si tu utilises une <nav> + liens ou un menu button (role="menu") : (a) la barre principale du site (Accueil, Cours, Blog), (b) le bouton « ⋯ » d’une ligne de tableau (Modifier, Supprimer), (c) un fil d’Ariane.

✅ Solution

(a) <nav> + liens : ce sont des navigations vers des pages, pas des actions. (b) Menu button (role="menu") : ce sont des actions sur un objet, navigables aux flèches, Échap pour fermer. (c) <nav> + liens (souvent <nav aria-label="Fil d'Ariane">) : encore de la navigation. Règle : on navigue → liens ; on agit → menu button.

Exercice 2 — Répare l’onglet. Ce composant d’onglets ne réagit pas aux flèches et met tous les onglets dans la tabulation. Cite les deux corrections clés.

<div role="tablist"> <button role="tab" tabindex="0">Profil</button> <button role="tab" tabindex="0">Sécurité</button> </div>

✅ Solution

(1) Roving tabindex : un seul onglet doit avoir tabindex="0" (l’actif), les autres tabindex="-1", sinon Tab s’arrête sur chacun au lieu de traiter la barre comme un tout. (2) Gérer les flèches : câbler ArrowLeft/ArrowRight pour changer d’onglet, mettre à jour aria-selected et appeler .focus() sur le nouvel onglet actif. Il manque aussi les aria-controls/aria-selected reliant chaque tab à son tabpanel.

🧠 Quiz de révision

1. Quel est le seul attribut d’état d’une disclosure ?

aria-expanded sur le <button> déclencheur : true quand le contenu est visible, false sinon. Il doit être piloté par le même état que la visibilité du bloc. L’élément natif <details>/<summary> fait ça sans JavaScript.

2. Pourquoi ne faut-il pas mettre role="menu" sur une navigation de site ?

Parce que role="menu" désigne un menu d’application (des actions navigables aux flèches, où Tab sort du menu), pas une liste de liens. L’appliquer à une nav casse le comportement attendu : le lecteur d’écran annonce un menu d’actions et l’utilisateur cherche des flèches là où Tab suffisait. Une nav = <nav> + <ul> + <a>.

3. Quel modèle de focus utilisent les onglets, et pourquoi ?

Le roving tabindex : un seul tab a tabindex="0", les autres tabindex="-1". Les flèches déplacent ce point d’arrêt (et le focus réel via .focus()). Ça permet de traiter la barre d’onglets comme un tout : on y entre au Tab, on choisit aux flèches, on ressort au Tab.

4. Quels rôles composent un système d’onglets ?

role="tablist" sur le conteneur, role="tab" sur chaque onglet (avec aria-selected et aria-controls), role="tabpanel" sur chaque panneau (avec aria-labelledby pointant son onglet). Un seul onglet sélectionné, un seul panneau visible à la fois.

5. En quoi un accordéon diffère-t-il des onglets, côté clavier ?

L’accordéon est une pile de disclosures : chaque en-tête est un <button aria-expanded> dans le flux normal du Tab, activé par Entrée/Espace. Il n’exige pas de navigation aux flèches (contrairement aux onglets). Plusieurs sections peuvent être ouvertes simultanément — c’est un choix de design.


Chapitre suivant : Combobox & listbox — l’autocomplétion, le pattern le plus complexe, et pourquoi une lib éprouvée y est souvent le bon choix.

Last updated on