# Accordéon masqué en CSS : pourquoi il reste annoncé aux lecteurs d’écran

> Ce qu'on observe sur des foires aux questions d'assurance, pourquoi le CSS seul ne suffit pas à masquer un panneau, et la correction avec aria-expanded et l'attribut hidden.

- Auteur : WordPress Développement
- Publié le : 2024-09-01
- Mis à jour le : 2024-09-01
- Catégorie : Accessibilité
- URL : https://www.wpmoderne.fr/accessibilite/accordeon-masque-css-lecteurs-ecran/

## L’essentiel

- display none en CSS seul ne suffit jamais à masquer proprement
- aria-expanded décrit l'état, hidden masque réellement
- Les deux doivent rester synchronisés à chaque clic

Pourquoi un lecteur d'écran annonce-t-il le contenu d'un panneau d'accordéon fermé, alors que celui-ci est parfaitement invisible à l'écran ? C'est la question posée par l'équipe support d'un courtier en assurance auto, après plusieurs retours d'utilisateurs signalant une foire aux questions « qui répète tout en boucle » avec leur lecteur d'écran.

La FAQ comptait trente-quatre questions réparties en accordéon, chacune affichant sa réponse au clic sur l'intitulé de la question. Le composant avait été codé sur mesure quelques mois plus tôt, sans plugin, avec une animation de repli en CSS jugée suffisante par l'équipe qui l'avait développé.

## Ce qu'on observe : un panneau fermé qui reste lu intégralement

Le code initial masquait le panneau de réponse uniquement via une classe CSS appliquant `max-height: 0` et `overflow: hidden`, une technique courante pour animer l'ouverture et la fermeture en douceur.

```
.reponse {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.3s ease;
}
.reponse.ouverte {
  max-height: 500px;
}
```

Visuellement, cette technique fonctionne parfaitement : le panneau fermé n'occupe aucune place visible et l'animation d'ouverture est fluide. Mais un lecteur d'écran ne s'appuie pas sur le rendu visuel pour décider quoi annoncer : il lit le contenu du DOM tel qu'il existe, indépendamment de sa hauteur CSS. Résultat, les trente-quatre réponses de la FAQ étaient toutes lues à la suite lors d'une navigation séquentielle, qu'elles soient ouvertes ou non à l'écran.

## Pourquoi le CSS seul ne suffit jamais

Un contenu masqué par `max-height: 0`, `opacity: 0` ou même `visibility: hidden` selon les cas reste présent dans l'arbre d'accessibilité du navigateur, sauf exception. Seules certaines propriétés précises, en particulier `display: none` ou l'attribut HTML natif `hidden`, retirent effectivement un élément de cet arbre et empêchent les technologies d'assistance de le restituer.

Le choix de `max-height` plutôt que `display: none` dans ce composant s'expliquait par la volonté d'animer la transition, une contrainte technique légitime, mais qui avait fait perdre de vue la conséquence sur l'accessibilité du composant.

> L'essentiel à retenir : display none en CSS seul ne suffit jamais à masquer proprement ; aria-expanded décrit l'état, hidden masque réellement ; Les deux doivent rester synchronisés à chaque clic

## Correctif : synchroniser aria-expanded et hidden

La correction retenue combine deux mécanismes distincts et complémentaires : `aria-expanded` sur le bouton de la question, qui décrit l'état ouvert ou fermé sans rien masquer, et l'attribut `hidden` sur le panneau de réponse, qui le retire réellement de l'arbre d'accessibilité quand il est fermé.

```
<h3>
  <button aria-expanded="false" aria-controls="reponse-12">
    Que couvre exactement la garantie bris de glace ?
  </button>
</h3>
<div id="reponse-12" class="reponse" hidden>
  <p>La garantie bris de glace couvre le pare-brise…</p>
</div>
```

Le script bascule les deux attributs ensemble à chaque clic, sans jamais laisser l'un des deux en retard sur l'autre : retirer `hidden` au moment de l'ouverture, puis laisser l'animation CSS existante gérer la transition visuelle sur un panneau désormais présent dans le DOM.

```
bouton.addEventListener('click', () => {
  const ouvert = bouton.getAttribute('aria-expanded') === 'true';
  bouton.setAttribute('aria-expanded', String(!ouvert));
  panneau.hidden = ouvert;
});
```

## Ce que cette correction ne change pas

- L'animation de repli en `max-height` reste utilisée pour l'effet visuel, en complément de `hidden`, pas à sa place.
- La structure en `<h3>` suivie d'un bouton, déjà correcte dans le composant d'origine, n'a pas eu besoin d'être modifiée.
- Le comportement au clic à la souris reste identique pour les utilisateurs qui n'utilisent aucune technologie d'assistance.

### Un piège voisin à surveiller

Le même défaut se rencontre fréquemment avec des menus déroulants ou des info-bulles masqués uniquement par une opacité nulle associée à un positionnement hors écran : la technique visuelle fonctionne, mais le contenu reste pleinement présent et lu par les technologies d'assistance, exactement comme dans cet accordéon.

> Un panneau invisible à l'œil n'est pas nécessairement invisible à l'arbre d'accessibilité : ce sont deux mécanismes distincts, et seul le second garantit qu'un contenu fermé reste réellement ignoré.

## En résumé

Trente-quatre questions de FAQ étaient toutes lues intégralement par un lecteur d'écran, quel que soit leur état d'ouverture, à cause d'un masquage CSS qui ne retire jamais un élément de l'arbre d'accessibilité. La combinaison de `aria-expanded`, qui décrit l'état, et de `hidden`, qui masque réellement, résout ce défaut sans renoncer à l'animation existante, à condition de synchroniser les deux attributs à chaque interaction.
