Les composants interactifs de votre site — menus déroulants, accordéons, onglets — fonctionnent-ils correctement avec les logiciels de lecture ?
Critère officiel 7.1 — Chaque script est-il, si nécessaire, compatible avec les technologies d’assistance ?
Pourquoi c'est important
Une personne aveugle navigue dans un accordéon FAQ ou un menu déroulant sans voir l'état ouvert ou fermé ni les options disponibles. Si le développeur n'a pas déclaré ces états dans le code, le logiciel de lecture annonce uniquement « bouton » sans préciser s'il est développé ou réduit, ni quel contenu il contient.
Exemples concrets
Ce qui est conforme
L'accordéon FAQ annonce correctement : « Bouton : Comment inscrire mon enfant à la cantine ? — Réduit. Appuyez sur Entrée pour développer. » Après activation : « Développé. » Le contenu est lu. L'état du composant est toujours communiqué.
Ce qui pose problème
Le même accordéon annonce seulement : « Bouton. Bouton. Bouton. » Le logiciel de lecture ne signale pas que ces boutons ouvrent du contenu caché, ni quel contenu ils contrôlent.
Comment agir
Lors de tout ajout de composant interactif (menu, accordéon, carrousel, onglets), incluez dans le cahier des charges : « Les composants interactifs doivent implémenter les attributs ARIA d'état (aria-expanded, aria-selected, aria-haspopup) conformément au RGAA 4.1. » Test rapide : appuyez sur Tab pour atteindre le composant, puis Entrée — s'il ne réagit pas, le problème est confirmé.
Règles clés
- Règle 1 de l'ARIA : utiliser l'élément HTML natif en premier. <button> avant role="button", <a> avant role="link".
- Tout élément interactif custom doit avoir : un rôle ARIA (role), un nom accessible (aria-label ou contenu texte), et un état si pertinent (aria-expanded, aria-selected, aria-checked…).
- aria-controls doit référencer un id existant dans le même document : sinon il vaut mieux l'omettre.
- Le pattern Disclosure (accordéon, menu déroulant de navigation) est différent du pattern Menu/Menubar : ne pas confondre.
- Une modal doit piéger le focus (Tab/Shift+Tab circulent dans la modal uniquement) et se fermer avec Échap.
Erreurs fréquentes
- Déclencheur de menu ou d'accordéon sur une <div> ou <span> sans role="button"
- aria-expanded absent ou non mis à jour à l'ouverture/fermeture
- aria-controls renseigné mais référençant un id inexistant dans le DOM
- role="menu" utilisé pour une navigation de site (à éviter : utiliser le pattern Disclosure Navigation à la place)
- Focus non géré à l'ouverture d'une modal : le focus reste sur le déclencheur au lieu de passer à l'intérieur
- Focus non rendu à l'élément déclencheur à la fermeture d'une modal
- Absence de piège de focus (focus trap) dans une modal ouverte
- Composant uniquement activable au clic : pas de support Enter/Espace sur les éléments role="button"
- aria-haspopup utilisé sur un bouton contrôlant une liste de liens : la valeur doit correspondre au rôle réel du conteneur (menu, listbox, tree, grid, dialog). Pour une sous-navigation, utiliser aria-expanded à la place
- aria-haspopup='true' sur un bouton de navigation : la valeur 'true' équivaut à 'menu' par défaut, créant les mêmes problèmes que aria-haspopup='menu'
- Confusion navigation/menu : utiliser aria-haspopup uniquement pour de vrais menus d'actions (comme les menus d'application desktop), pas pour des listes de liens de navigation
- aria-labelledby ou aria-describedby référençant un ID qui n'existe pas dans le DOM, l'attribut devient inopérant et l'élément se rabat sur son nom accessible par défaut | aria-labelledby avec références multiples ('btn1 btn2') où une seule référence existe. Seul le texte de l'élément existant est concaténé, l'ID manquant est ignoré silencieusement | aria-labelledby référençant un élément masqué par aria-hidden='true' : fonctionne correctement, le texte est extrait malgré le masquage pour les technologies d'assistance (AT)
- Overlay ou script de remédiation automatique (type UserWay, AccessiBe) ajoutant aria-expanded ou aria-label via JavaScript externe sur un composant React/Vue/Angular : les attributs ARIA injectés après rendu sont effacés à chaque re-rendu du framework : l'état ARIA redevient incorrect de façon silencieuse et intermittente, sans erreur visible dans l'outil de test
- aria-label ou aria-labelledby appliqué sur un élément générique (div, span) : le rôle implicite 'generic' interdit le nommage selon la spec ARIA : le label est ignoré par JAWS, NVDA, VoiceOver iOS et TalkBack/Firefox, mais annoncé de façon incohérente par VoiceOver macOS ('News, group') et Narrator ('News, group, content'), rendant le comportement non prédictible entre AT. Exception : <section aria-label='…'> change le rôle implicite de generic à region (landmark valide) ; <div popover aria-label='…'> labellise un group et non un generic.
- aria-current='page' appliqué à la fois sur le lien de la page courante ET sur ses entrées parentes dans un méga-menu : la spec WAI-ARIA 1.2 recommande de ne marquer qu'un seul élément d'un groupe comme 'current' (SHOULD). Utiliser aria-current='location' sur les ancêtres et aria-current='page' uniquement sur le lien exact de la page courante. Ne pas utiliser aria-current='true' sur plusieurs niveaux simultanément : la valeur générique 'current' répétée plusieurs fois dans le même groupe crée une confusion pour l'utilisateur de lecteur d'écran qui ne peut pas distinguer sa destination réelle de ses sections parentes.
- Infobulle (tooltip) porteuse d'information non restituée par le lecteur d'écran : l'infobulle est déclenchée au survol souris uniquement et son contenu n'est pas associé programmatiquement à l'élément déclencheur. Correctif : utiliser aria-describedby sur l'élément déclencheur en pointant vers l'id du contenu de l'infobulle. Le lecteur d'écran restituera le texte à la suite de l'intitulé de l'élément sans que l'infobulle ait besoin d'être focusable.
- aria-label appliqué sur un <span>, <i> ou <div> portant une icône de font-library (FontAwesome, Material Icons) sans role='img' : sans ce rôle, le lecteur d'écran ignore le label sur ces éléments génériques. La règle est : si l'icône doit être restituée comme une image avec un nom accessible, elle doit porter role='img' en plus de aria-label. Si le bouton ou le lien englobant porte déjà l'aria-label, l'icône enfant doit recevoir aria-hidden='true' pour éviter la double annonce ('Recherche, image, bouton' au lieu de 'Recherche, bouton').
- Tooltip implémenté avec aria-describedby sur un lien (<a href>) : Narrator (Edge) ne restitue pas aria-describedby sur les liens : bug connu non corrigé depuis plus de 4 ans. Le contenu du tooltip est silencieux pour 37% des utilisateurs Windows concernés. Attendre le correctif Microsoft ou utiliser une alternative (dialog non-modal, footnote accessible).
- Tooltip sur un élément intégré dans un bloc de texte (paragraphe) avec aria-describedby : NVDA et JAWS en mode Browse (navigation par flèches) n'annoncent pas le contenu du tooltip lorsque le déclencheur est entouré de texte courant. Le tooltip fonctionne en mode Focus (Tab) pour les éléments autonomes, mais pas en mode Browse pour les éléments dans un paragraphe. Aucun correctif propre n'existe à ce jour : envisager les footnotes accessibles pour les définitions en contexte textuel.
- Focus déplacé à l'ouverture de la modal vers le premier élément focusable (ex. : bouton 'Fermer') au lieu du titre du dialog : les tests utilisateurs montrent que l'utilisateur de lecteur d'écran reçoit une action sans contexte et doit explorer la modal pour comprendre son objet. La bonne pratique est de déplacer le focus sur le titre de la modal (h2 avec tabindex='-1') via un script sur l'événement 'toggle' du <dialog>, ce qui annonce d'abord le nom de la fenêtre modale avant toute interaction.
- Attribut closedby='none' sur un <dialog> sans bouton de fermeture explicite : si ni la touche Échap ni le clic en dehors ne ferment la boîte de dialogue, un bouton de fermeture accessible au clavier et aux technologies d'assistance est impératif. Sans lui, l'utilisateur clavier et le lecteur d'écran sont bloqués dans la modal sans aucune issue.
- aria-label résiduel (copié-collé ou mal supprimé) écrasant silencieusement le texte visible d'un bouton ou d'un déclencheur : la hiérarchie de calcul du nom accessible place aria-label avant le texte contenu dans l'élément. Un bouton <button aria-label='Actualiser'>Envoyer</button> sera annoncé 'Actualiser' par les AT même si 'Envoyer' est visuellement affiché. Ce type d'erreur est invisible à l'œil nu et n'est détecté que par un outil comme WAVE (onglet Order) ou le panneau Accessibilité des DevTools Chrome. Rappel de la hiérarchie : aria-labelledby > aria-label > HTML natif (label, alt, caption) > texte du contenu > title.
Exemples de code
accordéon (déclencheur non sémantique)
✗ Non conforme<div class="accordion-trigger" onclick="toggle()">FAQ : Comment commander ?</div>
<div id="panel-1" style="display:none">...</div>Une <div> cliquable n'est pas focusable au clavier et n'a aucun rôle. Le lecteur d'écran ne sait pas que c'est un déclencheur interactif.
accordéon (déclencheur accessible)
✓ Conforme<button
type="button"
aria-expanded="false"
aria-controls="panel-1"
>FAQ : Comment commander ?</button>
<div id="panel-1" hidden>...</div><button> est focusable et activable au clavier nativement. aria-expanded annonce l'état ouvert/fermé. aria-controls lie le bouton au panneau. hidden masque le contenu aux AT quand il est fermé.
navigation avec sous-menus (pattern incorrect)
✗ Non conforme<nav>
<ul role="menubar">
<li role="menuitem">Produits
<ul role="menu">
<li role="menuitem"><a href="/logiciels">Logiciels</a></li>
</ul>
</li>
</ul>
</nav>role="menu" et role="menuitem" impliquent des comportements clavier spécifiques aux applications desktop (flèches directionnelles). Les lecteurs d'écran passent en mode application, ce qui désactive la navigation par lecture. À proscrire pour une navigation de site.
navigation avec sous-menus (pattern Disclosure)
✓ Conforme<nav aria-label="Navigation principale">
<ul>
<li>
<a href="/produits">Produits</a>
<button
type="button"
aria-expanded="false"
aria-controls="sous-menu-produits"
aria-label="Sous-menu Produits"
>▾</button>
<ul id="sous-menu-produits" hidden>
<li><a href="/logiciels">Logiciels</a></li>
</ul>
</li>
</ul>
</nav>Pattern Disclosure Navigation (APG recommandé). Pas de role="menu" : navigation standard par Tab. Le bouton séparé gère l'ouverture du sous-menu sans quitter le lien parent.
modal (gestion du focus)
✗ Non conforme// À l'ouverture : rien
document.getElementById('modal').style.display = 'block';Le focus reste sur le déclencheur. L'utilisateur de lecteur d'écran ne sait pas que la modal est ouverte, et ne peut pas interagir avec son contenu.
modal (gestion du focus)
✓ Conformefunction ouvrirModal(triggerEl, modalEl) {
modalEl.removeAttribute('hidden');
// Déplacer le focus sur le premier élément focusable
const premierFocusable = modalEl.querySelector(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
premierFocusable?.focus();
// Mémoriser le déclencheur pour restaurer le focus
modalEl._trigger = triggerEl;
}
function fermerModal(modalEl) {
modalEl.setAttribute('hidden', '');
// Rendre le focus au déclencheur
modalEl._trigger?.focus();
}À l'ouverture, le focus passe au premier élément focusable de la modal. À la fermeture, il retourne sur le déclencheur : l'utilisateur reprend là où il était.
Référence WCAG : 2.5.3, 4.1.2