Composant · Formulaires et saisie

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

Interaction clavier attendue

ToucheAction
Flèche basOuvre la liste si elle est fermée ; sinon active l'option suivante.
Flèche hautActive l'option précédente. En option : ouvre la liste et active la dernière option.
EntréeValide la suggestion ou l'option active, puis ferme la liste.
ÉchapFerme la liste sans valider (et peut effacer la valeur).
Alt + Flèche bas / hautEn option : affiche ou masque la liste sans déplacer la sélection.
Début / FinEn option : active la première ou la dernière option.
TabFerme 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, et aria-expanded posé 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-live sur 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 scrollIntoView sur 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ôler document.activeElement dans 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 :

  1. 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.
  2. Dans la console, liste ouverte et une option active : document.activeElement doit être l'input. Si c'est un <li>, le motif est cassé.
  3. Défilement : descendre aux flèches au-delà des options visibles ; l'option active doit défiler dans la zone visible.
  4. 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-live est 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

Toutes les fiches composants accessibles