Disclosure accessible : aria-expanded, details et RGAA
Le contrat du contenu dépliable (afficher/masquer) conforme RGAA : details/summary natif, aria-expanded sur le bouton et panneau masqué par hidden, jamais en CSS visuel.
Un contenu dépliable (disclosure, « afficher/masquer ») est un bouton qui contrôle la visibilité d'une zone de contenu : l'activer affiche ou masque la zone associée. C'est le motif fondation des interfaces repliables : l'accordéon n'est qu'une pile de contenus dépliables coiffés de titres. L'élément natif à préférer est le couple <details>/<summary>, qui fournit sans JavaScript la sémantique de bouton, l'état déplié/replié, les touches Entrée et Espace et le basculement d'affichage ; sinon, un <button aria-expanded> associé à un panneau masqué par l'attribut hidden. La simplicité du motif est trompeuse : ses deux défauts les plus répandus (état sur le mauvais élément, masquage purement visuel) sont invisibles à l'écran comme au scanner.
Quand ce motif est le mauvais choix
Le contenu dépliable convient à du contenu quelconque : texte, formulaire, paragraphe. Cas fréquents où un autre motif est correct :
- Le panneau liste des actions (Modifier, Dupliquer, Supprimer) : bouton de menu (
aria-haspopup+role="menu"), avec sa navigation par flèches. Utiliser une sémantique de menu pour un simple afficher/masquer impose des flèches sur du contenu qui n'est pas un menu, et inversement. - Le panneau liste des choix qui fixent une valeur : liste déroulante native
<select>, combobox ou listbox. - Plusieurs sections coiffées chacune d'un titre : accordéon, qui ajoute la structure de titres à la pile de contenus dépliables.
- Une courte description au survol ou au focus d'un élément : infobulle (
role="tooltip").
Critères RGAA applicables
- Critère 7.1 : compatibilité des scripts avec les technologies d'assistance. L'état déplié ou replié doit être exposé via
aria-expandedsur le bouton ; un chevron visuel qui pivote ne transmet rien aux technologies d'assistance. - Critère 7.3 : contrôle par le clavier et tout dispositif de pointage. Le déclencheur doit être un vrai
<button>, pour que Entrée et Espace fonctionnent sans gestionnaire clavier ajouté. - Critère 10.7 : prise de focus visible. Le bouton du contenu dépliable doit montrer un indicateur de focus visible.
- Critère 10.8 : contenus cachés. Le panneau replié doit être un contenu caché au sens du RGAA : masqué via l'attribut
hiddenoudisplay: none, donc ignoré des technologies d'assistance, et rendu restituable par l'action sur le bouton (test 10.8.1). Un masquage purement visuel laisse le contenu restitué et tabbable.
Interaction clavier attendue
| Touche | Action |
|---|---|
| Entrée | Bascule la visibilité du panneau ; le focus reste sur le bouton. |
| Espace | Bascule la visibilité du panneau ; le focus reste sur le bouton. |
C'est tout : aucune navigation par flèches n'appartient à ce motif. Si votre disclosure exige des flèches, c'est probablement qu'un autre motif (menu, onglets) a été choisi à tort ou à moitié.
Rôles et attributs ARIA
La version zéro JavaScript d'abord :
<!-- À préférer. Le navigateur fournit la sémantique de bouton, l'état
déplié/replié, Entrée/Espace et le basculement. Rien à casser. -->
<details>
<summary>Qu'est-ce que le RGAA ?</summary>
<p>Le référentiel général d'amélioration de l'accessibilité.</p>
</details>
Réservez la version ARIA aux cas où vous devez animer l'ouverture ou piloter l'état depuis l'extérieur. La structure repose sur un <button> natif (rôle button implicite, ne pas l'ajouter) portant aria-expanded="true" ou "false". Cet attribut va sur le BOUTON, jamais sur le panneau : il décrit ce que fait le contrôle. aria-controls vers l'id du panneau est optionnel (support limité par les lecteurs d'écran : un bonus, jamais le vecteur de l'état) ; le panneau doit alors toujours exister dans le DOM pour que la référence se résolve.
<button type="button" aria-expanded="false" aria-controls="panneau-faq-1">
<svg aria-hidden="true" focusable="false" width="12" height="12">
<use href="#icone-chevron"/>
</svg>
Qu'est-ce que le RGAA ?
</button>
<!-- Toujours dans le DOM, basculé par hidden : le panneau doit exister pour
que aria-controls se résolve, et hidden le retire À LA FOIS de l'arbre
d'accessibilité et de l'ordre de tabulation (critère 10.8). -->
<div id="panneau-faq-1" hidden>
<p>Le référentiel général d'amélioration de l'accessibilité.</p>
</div>
const bouton = document.querySelector('[aria-controls="panneau-faq-1"]');
const panneau = document.getElementById('panneau-faq-1');
// Aucun gestionnaire keydown : un <button> natif déclenche déjà click
// sur Entrée et Espace.
bouton.addEventListener('click', () => {
const estDeplie = bouton.getAttribute('aria-expanded') === 'true';
bouton.setAttribute('aria-expanded', String(!estDeplie));
panneau.hidden = estDeplie;
});
Côté styles, pilotez le chevron depuis l'attribut lui-même ([aria-expanded="true"] .chevron) plutôt que depuis une classe miroir : l'attribut est la source de vérité, le visuel et l'état annoncé ne peuvent pas diverger. Gardez enfin le libellé stable (« Options de livraison ») et laissez aria-expanded porter l'état : basculer le libellé (« Afficher »/« Masquer ») en plus de l'état fait annoncer l'information deux fois, de façon contradictoire.
À noter : l'attribut HTML popover prend désormais en charge une partie de cette mécanique d'ouverture et de fermeture (état exposé, Échap, clic extérieur, affichage au premier plan) sans JavaScript. Pour ce qu'il gère réellement et ce qui reste à votre charge : Popover natif et focusgroup : sortir d'ARIA sans casser le RGAA.
À ne pas faire :
<!-- aria-expanded sur le panneau : le bouton est annoncé sans état,
l'utilisateur n'apprend jamais qu'il déplie quelque chose.
L'attribut décrit le CONTRÔLE. -->
<button type="button">Qu'est-ce que le RGAA ?</button>
<div id="panneau" aria-expanded="false">…</div>
<!-- div cliquable : non focalisable, non annoncé comme bouton,
Entrée et Espace inopérants. -->
<div class="faq-toggle" onclick="basculer()">Qu'est-ce que le RGAA ?</div>
<!-- aria-hidden sur un panneau resté dans l'ordre de tabulation : le focus
atterrit dans un contenu que le lecteur d'écran déclare absent.
Utiliser hidden. -->
<div id="panneau" aria-hidden="true">
<a href="/guide">En savoir plus</a>
</div>
<!-- Libellé basculé EN PLUS de l'état : annoncé « Masquer, déplié » puis
« Afficher, replié ». Nom stable, l'état suffit. -->
<button type="button" aria-expanded="true">Masquer le détail</button>
/* À ne pas faire : masquer le panneau replié visuellement seulement.
Il reste dans l'arbre d'accessibilité et dans l'ordre de tabulation :
l'utilisateur clavier tabule dans des liens invisibles (critère 10.8). */
.panneau--replie {
opacity: 0;
height: 0;
overflow: hidden;
}
Défauts fréquents et impact utilisateur
aria-expandedsur le panneau au lieu du bouton : le bouton est annoncé sans état ; l'utilisateur de lecteur d'écran n'apprend jamais qu'il déplie quelque chose. C'est le défaut le plus fréquent de ce composant.<div>cliquable à la place d'un bouton : non focalisable, non annoncé comme bouton, Entrée et Espace inopérants ; le contenu dépliable n'existe pas pour l'utilisateur clavier.- Chevron visuel seul, sans
aria-expanded: les utilisateurs voyants voient l'état, les utilisateurs de lecteur d'écran entendent « bouton » sans indication qu'il déplie quoi que ce soit. - Masquage visuel du panneau replié (
opacity: 0, hauteur nulle, positionnement hors écran) : le contenu reste restitué et tabbable ; l'utilisateur clavier tabule dans des liens invisibles. aria-hidden="true"sur un panneau resté tabbable : le focus atterrit dans un contenu que le lecteur d'écran déclare absent.- Libellé basculé en plus de l'état : annoncé « Masquer, déplié » puis « Afficher, replié » ; le nom et l'état disent deux fois la même chose, en sens inverse.
Ce que les outils automatiques ne détectent pas
Les outils automatiques repèrent un aria-hidden posé sur un contenu focalisable et un bouton sans nom accessible. En revanche, restent des vérifications manuelles :
aria-expandedposé sur le panneau plutôt que sur le bouton : le balisage est valide en apparence, seul un test au lecteur d'écran révèle l'absence d'état annoncé ;- un panneau replié masqué en
opacity: 0ou hauteur nulle : visuellement correct, mais après repli, Tab doit sauter entièrement le panneau, ce qui se vérifie au clavier ; - la cohérence de l'annonce : « nom, bouton, replié » puis « déplié » après activation, une seule fois, sans doublon d'état.
Ce sont précisément les deux défauts qui partent le plus souvent en production : tous deux paraissent corrects à l'écran et dans le code.
Vérifier ce composant
Protocole manuel rapide, dans cet ordre :
- Au clavier seul : Tab jusqu'au bouton, Espace : le panneau s'ouvre. Tab : le focus doit entrer dans le panneau. Replier, puis Tab : le focus doit sauter entièrement le panneau. Si vous tabulez encore dans du contenu invisible, le masquage est purement visuel.
- Au lecteur d'écran : attendre « nom, bouton, replié », puis « déplié » après activation. Aucun état annoncé :
aria-expandedest sur le mauvais élément. État annoncé deux fois : le libellé bascule en plus de l'état. - À la souris : le clic sur le bouton (icône comprise) bascule le panneau, exactement comme le clavier.
Pour outiller ces vérifications : le simulateur de lecteur d'écran montre l'arbre d'accessibilité et l'état exposé du bouton, et le testeur de focus visible vérifie l'indicateur de focus du déclencheur. Le guide Tester au clavier détaille la méthode complète de parcours.
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