Combobox accessible : critères RGAA, clavier et ARIA
Rendre un combobox conforme RGAA : quand préférer un select natif, interactions clavier attendues, rôles ARIA (aria-expanded, aria-activedescendant) et défauts fréquents.
Le combobox est un champ de saisie associé à une liste déroulante de suggestions (un listbox) que la frappe filtre en temps réel. C'est le motif le plus complexe du WAI-ARIA APG, et le plus souvent cassé en production : la version « qui a l'air de marcher » à la souris est presque toujours inutilisable au clavier ou au lecteur d'écran. Bonne nouvelle : dans la majorité des cas, vous n'avez pas besoin de le construire.
Quand ce motif est le mauvais choix
Avant d'écrire la moindre ligne d'ARIA, posez la question du besoin réel :
- Choisir une valeur dans une liste connue : un
<select>natif est complet au clavier, au lecteur d'écran et sur mobile, sans une ligne de JavaScript. « Le select ne colle pas à la maquette » se résout presque toujours en CSS. - Liste longue que la saisie doit filtrer, ou texte libre avec suggestions : c'est le vrai cas d'usage du combobox éditable, avec
aria-autocomplete="list". - Complétion du texte directement dans le champ en plus de la liste :
aria-autocomplete="both". - Sélection multiple avec étiquettes amovibles (chips) : un combobox accompagné de boutons de suppression distincts, chacun avec son propre intitulé.
<datalist>ressemble à la solution native de l'autocomplétion, mais sa restitution par les lecteurs d'écran est trop inégale pour s'y fier sur un parcours critique.
La règle d'or : ne construisez le motif ARIA que si le filtrage à la saisie est réellement nécessaire. La plupart des « selects custom » n'existent que pour un rayon de bordure.
Critères RGAA applicables
- Critère 7.1 : compatibilité des scripts avec les technologies d'assistance. Les rôles
combobox,listboxetoption, l'étataria-expandedet l'option active doivent être exposés aux technologies d'assistance. Rien de tout cela n'est fourni gratuitement, contrairement à un élément natif. - Critère 7.3 : script contrôlable au clavier et par tout dispositif de pointage. Les flèches doivent ouvrir et parcourir la liste, Échap la fermer, Entrée sélectionner, et la souris doit permettre exactement les mêmes actions.
- Critère 7.5 : messages de statut. Le résultat du filtrage (« 3 résultats disponibles ») est un message de statut à restituer via une zone
role="status"(test 7.5.1). Sans elle, l'utilisateur de lecteur d'écran tape dans le silence. - Critère 10.7 : prise de focus visible. Le focus réel reste sur le champ et son indicateur doit rester visible. L'option active désignée par
aria-activedescendantdoit elle aussi être signalée visuellement, car:focus-visiblene se déclenche jamais sur elle. - Critère 11.1 : étiquette de champ de formulaire. Un élément portant
role="combobox"est un champ de formulaire au sens du RGAA et doit avoir une étiquette associée. Un placeholder n'en est pas une. - Critère 12.9 : pas de piège au clavier. Tab doit toujours fermer la liste et quitter le composant.
Interaction clavier attendue
| Touche | Action |
|---|---|
| Flèche bas | Ouvre la liste si elle est fermée ; sinon active l'option suivante. |
| Flèche haut | Active l'option précédente. En option : ouvre la liste et active la dernière option. |
| Entrée | Valide la suggestion ou l'option active, puis ferme la liste. |
| Échap | Ferme la liste sans valider (et peut effacer la valeur). |
| Alt + Flèche bas / haut | En option : affiche ou masque la liste sans déplacer la sélection. |
| Début / Fin | En option : active la première ou la dernière option. |
| Tab | Ferme la liste et quitte le champ. Ne doit jamais être intercepté. |
Le principe central du motif : le focus DOM ne quitte jamais le champ. Les options se parcourent aux flèches, jamais à Tab, et aucune option ne reçoit jamais le focus réel.
Rôles et attributs ARIA
Le rôle combobox se place sur l'<input> lui-même (ARIA 1.2). L'ancien motif ARIA 1.0, qui plaçait role="combobox" sur un <div> conteneur autour du champ, est obsolète et mal restitué par les lecteurs d'écran actuels, même s'il figure encore dans de nombreux tutoriels. aria-expanded, aria-controls et aria-activedescendant vivent aussi sur l'input :
<label for="ville">Ville</label>
<div class="combobox">
<!-- role, aria-expanded, aria-controls et aria-activedescendant vivent
TOUS sur l'input. Le conteneur ne sert qu'à la mise en page. -->
<input type="text" id="ville" role="combobox"
aria-expanded="false" aria-controls="ville-listbox"
aria-autocomplete="list" autocomplete="off">
<!-- Reste dans le DOM pour que aria-controls résolve ; hidden quand fermé. -->
<ul id="ville-listbox" role="listbox" aria-label="Villes" hidden>
<li id="ville-opt-1" role="option" aria-selected="false">Paris</li>
<li id="ville-opt-2" role="option" aria-selected="false">Pau</li>
</ul>
</div>
<!-- Zone de statut séparée, masquée visuellement. JAMAIS aria-live sur le
listbox lui-même : chaque frappe relirait la liste entière. -->
<div role="status" class="sr-only"></div>
aria-activedescendant désigne l'option visuellement active pendant que le focus réel reste sur le champ. Conséquences directes, toutes à votre charge :
function activerOption(index) {
options.forEach((option, i) =>
option.setAttribute('aria-selected', String(i === index)));
// Le focus DOM reste sur l'input : c'est aria-activedescendant qui indique
// aux technologies d'assistance quelle option est active. On n'appelle
// JAMAIS options[index].focus().
input.setAttribute('aria-activedescendant', options[index].id);
// Pas de focus réel = pas de défilement automatique par le navigateur.
options[index].scrollIntoView({ block: 'nearest' });
}
function fermer() {
input.setAttribute('aria-expanded', 'false');
listbox.hidden = true;
// À retirer, sinon le champ continue d'annoncer une option disparue.
input.removeAttribute('aria-activedescendant');
}
Côté styles, comme l'option active n'a pas le focus réel, :focus-visible ne se déclenche jamais sur elle : c'est [aria-selected="true"] que votre CSS doit mettre en évidence, avec un contraste suffisant, sinon l'utilisateur clavier regarde une liste qui ne bouge jamais.
À chaque filtrage, la zone role="status" reçoit le nombre de résultats (« 3 résultats disponibles », « Aucun résultat »). C'est elle, et jamais le listbox, qui porte l'annonce.
Défauts fréquents et impact utilisateur
- Motif ARIA 1.0 obsolète (
role="combobox"sur un conteneur autour de l'input) : les lecteurs d'écran actuels annoncent un groupe au lieu du champ, etaria-expandedposé sur un<div>n'est pas restitué. - Appeler
.focus()sur une option en plus d'aria-activedescendant: le bug emblématique du combobox. Le focus quitte le champ, la saisie cesse de fonctionner et le modèle du composant s'effondre pour le lecteur d'écran. Un seul mécanisme, jamais les deux. - Options avec
tabindex="0": chaque option devient un arrêt de tabulation et le focus DOM quitte le champ. Les options se parcourent aux flèches, jamais à Tab. aria-livesur le listbox : toutes les options sont réannoncées à chaque frappe. Taper « par » fait relire la liste trois fois.- Champ sans étiquette, avec un simple placeholder : annoncé « combobox » sans indication de sa fonction. Le placeholder disparaît à la saisie et n'est pas une étiquette (critère 11.1).
- Oublier
scrollIntoViewsur l'option active : l'utilisateur clavier descend dans la liste, mais l'option active reste invisible sous la ligne de flottaison. - Fermer la liste sur l'événement
blur: cliquer une option retire le focus du champ avant que le clic soit pris en compte ; la liste se ferme et la sélection n'a jamais lieu. Fermer sur Échap, Tab et un clic extérieur.
Ce que les outils automatiques ne détectent pas
- Le motif ARIA 1.0 obsolète (rôle sur un conteneur) est du balisage syntaxiquement valide : seule une vérification au lecteur d'écran révèle la restitution cassée.
- Le mélange focus réel +
aria-activedescendant: contrôlerdocument.activeElementdans la console, liste ouverte. Si le résultat est un<li>et non l'input, le motif est cassé. - L'absence d'annonce du nombre de résultats (critère 7.5) : seule une écoute au lecteur d'écran pendant la saisie permet de la constater.
- Le défilement manquant de l'option active et la fermeture correcte à Tab restent des vérifications manuelles au clavier.
Un scanner détecte en revanche une option hors de tout listbox, un champ sans étiquette ou un aria-activedescendant pointant vers un id inexistant : c'est utile, mais aucun des vrais bugs du combobox n'est dans cette liste.
Vérifier ce composant
Protocole manuel rapide, dans cet ordre :
- Au clavier seul : Flèche bas ouvre la liste et la mise en évidence se déplace visiblement ; Échap ferme ; Entrée sélectionne ; Tab ferme et sort du composant sans jamais bloquer.
- Dans la console, liste ouverte et une option active :
document.activeElementdoit être l'input. Si c'est un<li>, le motif est cassé. - Défilement : descendre aux flèches au-delà des options visibles ; l'option active doit défiler dans la zone visible.
- Au lecteur d'écran : la saisie doit annoncer le nombre de résultats, et chaque flèche doit annoncer l'option active. Si la liste entière est relue à chaque frappe,
aria-liveest posé au mauvais endroit.
Pour outiller ces vérifications : 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, et 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