Aller au contenu principal
Technique7 mars 20269 min

Accordéons accessibles : Guide d'implémentation complet

En Bref : L'essentiel à retenir

  • Un accordéon accessible nécessite un bouton dans un heading, aria-expanded pour l'état, et aria-controls liant le bouton au panneau.
  • La navigation clavier suit le pattern WAI-ARIA : Entrée/Espace pour basculer, flèches pour naviguer entre les en-têtes, Home/End pour les extrêmes.
  • L'élément HTML détails/summary est une alternative native mais son support accessibilité reste incomplet dans certains lecteurs d'écran.
  • Le panneau masqué doit utiliser l'attribut hidden plutôt que display:none pour une meilleure sémantique et compatibilité.
AccordéonARIAJavaScriptComposantsAccessibilité

Les accordéons permettent d'organiser du contenu dense en sections repliables. Mais leur accessibilité est souvent négligée, rendant les informations inaccessibles aux utilisateurs de technologies d'assistance. Voici comment créer des accordéons conformes au pattern WAI-ARIA.

Anatomie d'un accordéon accessible

Un accordéon se compose de :

  • En-têtes (headers) : boutons déclencheurs dans des headings
  • Panneaux (panels) : contenus associés à chaque en-tête
  • États : déplié/replié communiqués via ARIA
CODE
┌─────────────────────────────┐
│ [▼] Section 1 (bouton)      │ ← En-tête h3
├─────────────────────────────┤
│ Contenu de la section 1...  │ ← Panneau (visible)
│                             │
├─────────────────────────────┤
│ [▶] Section 2 (bouton)      │ ← En-tête h3
├─────────────────────────────┤
│ (contenu masqué)            │ ← Panneau (hidden)
└─────────────────────────────┘

Structure HTML de base

CODE
<div class="accordion">
  <h3>
    <button
      type="button"
      aria-expanded="true"
      aria-controls="panel-1"
      id="header-1">
      Qu'est-ce que l'accessibilité web ?
    </button>
  </h3>
  <div
    id="panel-1"
    role="region"
    aria-labelledby="header-1">
    <p>L'accessibilité web permet aux personnes handicapées
    d'utiliser le web de manière autonome...</p>
  </div>

  <h3>
    <button
      type="button"
      aria-expanded="false"
      aria-controls="panel-2"
      id="header-2">
      Pourquoi c'est important ?
    </button>
  </h3>
  <div
    id="panel-2"
    role="region"
    aria-labelledby="header-2"
    hidden>
    <p>En France, 12 millions de personnes sont en situation
    de handicap...</p>
  </div>
</div>

Explication des attributs

AttributÉlémentRôle
aria-expandedBoutonÉtat déplié/replié
aria-controlsBoutonID du panneau contrôlé
role="region"PanneauLandmark navigable
aria-labelledbyPanneauNommé par l'en-tête
hiddenPanneauMasque le contenu replié

[!NOTE] Le role="region" est optionnel et recommandé uniquement si votre accordéon a moins de 6 sections. Au-delà, cela crée trop de landmarks.

Le pattern WAI-ARIA définit ces interactions :

ToucheAction
TabPasse au prochain élément focusable
Entrée/EspaceBascule l'état déplié/replié
Flèche basFocus sur l'en-tête suivant
Flèche hautFocus sur l'en-tête précédent
HomeFocus sur le premier en-tête
EndFocus sur le dernier en-tête

Implémentation JavaScript

CODE
class Accordion {
  constructor(container) {
    this.container = container;
    this.headers = container.querySelectorAll('button');
    this.panels = container.querySelectorAll('[role="region"]');

    this.init();
  }

  init() {
    this.headers.forEach((header, index) => {
      header.addEventListener('click', () => this.toggle(index));
      header.addEventListener('keydown', (e) => this.handleKeydown(e, index));
    });
  }

  toggle(index) {
    const header = this.headers[index];
    const panel = this.panels[index];
    const isExpanded = header.getAttribute('aria-expanded') === 'true';

    header.setAttribute('aria-expanded', !isExpanded);
    panel.hidden = isExpanded;
  }

  handleKeydown(event, index) {
    const { key } = event;
    let newIndex;

    switch (key) {
      case 'ArrowDown':
        event.preventDefault();
        newIndex = (index + 1) % this.headers.length;
        this.headers[newIndex].focus();
        break;

      case 'ArrowUp':
        event.preventDefault();
        newIndex = (index - 1 + this.headers.length) % this.headers.length;
        this.headers[newIndex].focus();
        break;

      case 'Home':
        event.preventDefault();
        this.headers[0].focus();
        break;

      case 'End':
        event.preventDefault();
        this.headers[this.headers.length - 1].focus();
        break;
    }
  }
}

// Initialisation
document.querySelectorAll('.accordion').forEach(accordion => {
  new Accordion(accordion);
});

Variante : une seule section ouverte

Pour fermer automatiquement les autres sections :

CODE
toggle(index) {
  const header = this.headers[index];
  const panel = this.panels[index];
  const isExpanded = header.getAttribute('aria-expanded') === 'true';

  // Fermer toutes les sections
  this.headers.forEach((h, i) => {
    h.setAttribute('aria-expanded', 'false');
    this.panels[i].hidden = true;
  });

  // Ouvrir la section cliquée (si elle était fermée)
  if (!isExpanded) {
    header.setAttribute('aria-expanded', 'true');
    panel.hidden = false;
  }
}

[!TIP] Permettre plusieurs sections ouvertes est souvent plus accessible : l'utilisateur ne perd pas le contenu qu'il vient de lire.

Styliser l'indicateur visuel

Utilisez CSS pour l'icône, et masquez-la aux lecteurs d'écran :

CODE
<button aria-expanded="false">
  <span class="accordion-icon" aria-hidden="true"></span>
  Titre de la section
</button>
CODE
.accordion-icon::before {
  content: "+";
  margin-right: 0.5em;
}

button[aria-expanded="true"] .accordion-icon::before {
  content: "−";
}

L'état est communiqué par aria-expanded, l'icône est purement décorative.

Et details/summary natif ?

HTML5 propose <details> et <summary> pour les disclosures :

CODE
<details>
  <summary>Cliquez pour voir plus</summary>
  <p>Contenu révélé...</p>
</details>

Avantages

  • Aucun JavaScript requis
  • Support navigateur natif

Inconvénients actuels

  • Pas d'annonce d'état cohérente dans tous les lecteurs d'écran
  • Navigation clavier limitée (pas de flèches entre sections)
  • Stylisation CSS plus complexe

Recommandation : Pour des accordéons simples non critiques, <details> peut suffire. Pour une conformité RGAA stricte, utilisez le pattern ARIA complet.

Consultez notre article sur aria-expanded pour plus de détails.

Gestion du focus au déploiement

Quand un panneau s'ouvre, le focus reste sur le bouton. L'utilisateur peut ensuite naviguer dans le contenu avec Tab ou les flèches en mode lecture.

Ne déplacez pas automatiquement le focus dans le panneau, sauf si c'est un comportement attendu (comme une modale).

Animer l'ouverture/fermeture

Si vous animez la transition, respectez prefers-reduced-motion :

CODE
.accordion [role="region"] {
  transition: max-height 0.3s ease;
  overflow: hidden;
}

@media (prefers-reduced-motion: reduce) {
  .accordion [role="region"] {
    transition: none;
  }
}
CODE
// Alternative à hidden pour permettre l'animation
toggle(index) {
  const panel = this.panels[index];
  const isExpanded = header.getAttribute('aria-expanded') === 'true';

  if (isExpanded) {
    panel.style.maxHeight = null;
    panel.setAttribute('aria-hidden', 'true');
  } else {
    panel.style.maxHeight = panel.scrollHeight + 'px';
    panel.removeAttribute('aria-hidden');
  }
}

Erreurs courantes

1. Bouton hors du heading

CODE
<!-- Incorrect : le bouton doit être dans le heading -->
<h3>Titre de section</h3>
<button aria-expanded="false">Voir plus</button>

<!-- Correct -->
<h3>
  <button aria-expanded="false">Titre de section</button>
</h3>

2. Utiliser div au lieu de button

CODE
<!-- Incorrect : div n'est pas focusable nativement -->
<div class="accordion-header" onclick="toggle()">
  Section
</div>

<!-- Correct : button natif -->
<button type="button" aria-expanded="false">
  Section
</button>

3. Oublier d'initialiser aria-expanded

CODE
<!-- Incorrect : état indéterminé -->
<button>Section</button>

<!-- Correct : état initial défini -->
<button aria-expanded="false">Section</button>

Checklist accordéon accessible

  • Boutons dans des éléments heading (h2-h6)
  • aria-expanded sur chaque bouton
  • aria-controls pointant vers l'ID du panneau
  • Navigation clavier (Entrée, flèches, Home, End)
  • hidden ou équivalent sur les panneaux fermés
  • Icônes décoratives masquées (aria-hidden)
  • Respect de prefers-reduced-motion

Critères RGAA concernés

CritèreExigence
7.1Composants compatibles avec les technologies d'assistance
7.3Utilisables au clavier et à la souris
10.7Pas de prise de focus inattendue
12.8Navigation au clavier possible

Conclusion

Un accordéon accessible demande du soin dans l'implémentation, mais le pattern est bien documenté par le WAI-ARIA. Suivez la structure HTML, implémentez la navigation clavier complète, et testez avec un lecteur d'écran pour valider l'expérience.

Vérifiez l'accessibilité de vos composants interactifs avec RGAA Checker : notre outil analyse les attributs ARIA et la structure de vos pages.

Votre site est-il conforme ?

Ne prenez pas de risques avec l'accessibilité. Lancez un audit complet de votre site en quelques minutes et obtenez un rapport détaillé des corrections à apporter.