Composant · Afficher et masquer

Accordéon accessible : contrat ARIA, titres et critères RGAA

La référence RGAA de l'accordéon : titre encapsulant un bouton aria-expanded, niveau choisi selon le plan du document, panneaux masqués par hidden, flèches optionnelles sans capturer Tab.

Un accordéon est une pile verticale de sections dont chaque en-tête, un titre contenant un bouton, déplie ou replie le panneau de contenu associé. Autrement dit : un empilement de contenus dépliables auquel s'ajoute une structure de titres. Tout le contrat du contenu dépliable s'applique tel quel (aria-expanded sur le bouton, panneau masqué par hidden, libellé stable) ; cette fiche couvre ce que l'accordéon ajoute : les titres, et la navigation par flèches optionnelle. La structure de référence est un vrai titre encapsulant un vrai bouton (<h3><button aria-expanded>) ; pour un accordéon exclusif (un seul panneau ouvert à la fois), <details name="groupe"> fait le travail sans JavaScript, au prix d'une absence des <summary> dans le plan de titres.

Quand ce motif est le mauvais choix

  • Une seule zone à afficher/masquer, sans titre de section : un simple contenu dépliable suffit, sans structure de titres.
  • FAQ courte où le plan de titres importe peu : <details>/<summary> natif, zéro JavaScript.
  • Le panneau liste des actions : bouton de menu (aria-haspopup + role="menu"), pas un accordéon.
  • Contenus alternatifs de même nature dont un seul est consulté à la fois, présentés côte à côte : onglets (role="tablist"), qui ont une sémantique et un clavier différents.

Critères RGAA applicables

Interaction clavier attendue

ToucheAction
Entrée / EspaceDéplie le panneau replié ; peut replier les autres si un seul panneau peut être ouvert.
Tab / Maj + TabÉlément focalisable suivant ou précédent, dans la séquence normale de la page.
Flèche bas / Flèche hautEn option : en-tête d'accordéon suivant ou précédent, avec bouclage.
Début / FinEn option : premier ou dernier en-tête de l'accordéon.

Les flèches sont un accélérateur, jamais un remplacement : Tab reste le moyen d'entrer dans les panneaux et de sortir du composant.

Rôles et attributs ARIA

La version zéro JavaScript d'abord, pour un accordéon exclusif :

<h2>Questions fréquentes</h2>

<!-- Le name partagé rend les sections mutuellement exclusives : en ouvrir
     une referme les autres, sans JavaScript. Sans name, elles s'ouvrent
     indépendamment. -->
<details name="faq">
  <summary>Qu'est-ce que le RGAA ?</summary>
  <p>Le référentiel général d'amélioration de l'accessibilité.</p>
</details>

<details name="faq">
  <summary>Quel niveau de conformité s'applique ?</summary>
  <p>Le niveau double A par défaut.</p>
</details>

Réserve importante : <summary> est un bouton, pas un titre ; ces sections n'apparaissent pas dans le plan de titres. Pour une FAQ courte, c'est un compromis acceptable contre zéro JavaScript. Pour du contenu que l'utilisateur doit pouvoir parcourir par titres, la version ARIA s'impose. Le titre encapsule le bouton, jamais l'inverse, et le niveau de titre vient du plan du document, pas de la profondeur du composant :

<h2>Questions fréquentes</h2>

<!-- h3 parce que l'accordéon vit sous un h2. Le titre est l'entrée du plan,
     le bouton est le contrôle. Ne jamais les imbriquer dans l'autre sens. -->
<h3 class="accordeon__titre">
  <button type="button" id="btn-faq-1"
          aria-expanded="false" aria-controls="panneau-faq-1">
    Qu'est-ce que le RGAA ?
  </button>
</h3>
<div id="panneau-faq-1" role="region" aria-labelledby="btn-faq-1" hidden>
  <p>Le référentiel général d'amélioration de l'accessibilité.</p>
</div>
/* Le h3 ne porte aucun poids visuel : il existe pour le plan de titres,
   c'est le bouton qui porte le style. Ne pas SUPPRIMER le titre pour
   s'épargner son CSS. */
.accordeon__titre {
  margin: 0;
  font-size: inherit;
  font-weight: inherit;
}

role="region" avec aria-labelledby sur le panneau est recommandé pour un petit nombre de panneaux au contenu significatif ; sur un accordéon long, il inonde la liste des régions du lecteur d'écran. role="heading" + aria-level est un dernier recours : un vrai <h2> à <h6> les rend inutiles. Si vous ajoutez les flèches, elles s'ajoutent à Tab sans jamais le confisquer :

const accordeon = document.querySelector('.accordeon');
const boutons = [...accordeon.querySelectorAll('.accordeon__titre button')];

// Accélérateur OPTIONNEL : flèches entre les en-têtes. Tab n'est pas
// touché, le raccourci s'ajoute sans enfermer personne (critère 12.9).
accordeon.addEventListener('keydown', (event) => {
  const bouton = event.target.closest('.accordeon__titre button');
  if (!bouton) return;

  const index = boutons.indexOf(bouton);
  let cible = null;

  if (event.key === 'ArrowDown') cible = boutons[(index + 1) % boutons.length];
  if (event.key === 'ArrowUp') cible = boutons[(index - 1 + boutons.length) % boutons.length];
  if (event.key === 'Home') cible = boutons[0];
  if (event.key === 'End') cible = boutons.at(-1);

  if (cible) {
    event.preventDefault(); // uniquement pour les touches gérées, jamais en bloc
    cible.focus();
  }
});

Pour le pas-à-pas complet d'implémentation, du HTML de base au JavaScript de bascule : Accordéons accessibles : Guide d'implémentation complet.

À ne pas faire :

<!-- En-têtes en div : l'accordéon est absent du plan de titres. Un
     utilisateur de lecteur d'écran qui parcourt la page par titres
     (le mode de survol le plus courant) ne le trouve jamais. -->
<div class="accordeon__titre">
  <button type="button" aria-expanded="false">Qu'est-ce que le RGAA ?</button>
</div>

<!-- Titre DANS le bouton : un titre placé dans un contrôle cesse d'être
     une entrée du plan, et le nom accessible du bouton s'en trouve
     brouillé. Inverser : <h3><button>. -->
<button type="button" aria-expanded="false">
  <h3>Qu'est-ce que le RGAA ?</h3>
</button>

<!-- Niveau choisi selon la profondeur du composant : un h4 sous un h2
     saute un niveau (critère 9.1). Le niveau vient du plan du DOCUMENT. -->
<h2>Questions fréquentes</h2>
<h4><button type="button">Qu'est-ce que le RGAA ?</button></h4>
// À ne pas faire : capturer Tab pour circuler entre les en-têtes.
// Tab est le moyen de QUITTER le composant : le confisquer est un piège
// au clavier (critère 12.9).
accordeon.addEventListener('keydown', (event) => {
  if (event.key === 'Tab') {
    event.preventDefault();
    focusEnTeteSuivant();
  }
});

Défauts fréquents et impact utilisateur

  • En-têtes en <div> : l'accordéon est absent du plan de titres ; un utilisateur de lecteur d'écran qui parcourt la page par titres ne le trouve jamais.
  • Titre à l'intérieur du bouton (<button><h3>) : le titre cesse d'être une entrée du plan et le nom accessible du bouton est brouillé.
  • Niveau choisi selon la profondeur du composant : un <h4> sous un <h2> saute un niveau ; la hiérarchie devient incohérente pour la navigation par titres.
  • role="region" sur les 40 panneaux d'un long accordéon : chaque panneau devient une région ; la liste des régions, aide de navigation majeure du lecteur d'écran, devient inutilisable.
  • Tab capturé pour passer d'un en-tête à l'autre : Tab est le moyen de sortir du composant ; le confisquer enferme l'utilisateur clavier.
  • preventDefault() global sur keydown : tue Tab, Échap et les touches de navigation du lecteur d'écran qui transitent par le composant.

Ce que les outils automatiques ne détectent pas

Les outils automatiques repèrent les sauts de niveaux de titres et les titres vides. En revanche, restent des vérifications manuelles :

  • un en-tête en <div> simplement stylé comme un titre : rien à signaler pour un scanner, alors que l'accordéon disparaît du plan de titres ; le contrôle se fait dans l'arbre d'accessibilité ou au lecteur d'écran (l'annonce attendue est « libellé, replié, bouton, titre de niveau 3 ») ;
  • la navigation clavier réelle : Tab atteint chaque en-tête et peut toujours quitter l'accordéon, les flèches (si présentes) ne remplacent pas Tab ;
  • le repli effectif des panneaux pour les technologies d'assistance quand le masquage est fait en CSS visuel plutôt qu'avec hidden.

Vérifier ce composant

Protocole manuel rapide, dans cet ordre :

  1. Plan de titres : chaque en-tête d'accordéon doit apparaître dans l'arbre des titres, au bon niveau, sans saut. C'est la vérification propre à ce motif : si l'accordéon manque au plan, les en-têtes ne sont pas de vrais titres.
  2. Au clavier seul : Tab atteint chaque en-tête dans l'ordre et permet toujours de quitter l'accordéon. Si les flèches sont présentes, elles circulent entre les en-têtes ET Tab continue de fonctionner normalement.
  3. Au lecteur d'écran : naviguer par titres (touche H sous NVDA) ; vous devez atterrir sur chaque en-tête et entendre « libellé, replié, bouton, titre de niveau 3 ».
  4. Repli effectif : replier un panneau puis tabuler ; le focus doit sauter entièrement son contenu.

Pour outiller ces vérifications : le validateur de hiérarchie des titres visualise l'arbre h1-h6 de la page et détecte les sauts de niveaux, et le simulateur de lecteur d'écran donne un premier aperçu de la restitution. Le guide Tester avec un lecteur d'écran explique le test réel avec NVDA ou VoiceOver.

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