L'autocomplétion
Un champ, une liste de suggestions, et un lien entre les deux qui n'existe que dans les attributs. Le point dur n'est pas d'afficher la liste : c'est de la parcourir sans jamais sortir du champ. Démo, code, critères RGAA.
5 critères RGAA en jeu
Essayez-la
Ce que porte le champ, relevé dans le DOM à chaque frappe :
Tapez co, puis descendez avec la flèche bas. Le relevé du bas lit le
DOM à chaque frappe : le focus reste sur le champ de saisie, et c’est aria-activedescendant qui change pour désigner l’option courante. Continuez à
taper sans rien valider — la saisie reprend là où elle en était, parce qu’elle ne
l’a jamais quittée.
Cette démo existe aussi en page autonome.
L’erreur courante
La liste s’affiche, elle est jolie, et la flèche bas y envoie le focus.
<!-- À ne pas faire -->
<input type="text" oninput="filtrer()" placeholder="Thème du référentiel" />
<ul class="suggestions">
<li tabindex="0">Couleurs</li>
<li tabindex="0">Consultation</li>
</ul> // À ne pas faire : la flèche fait sortir du champ
champ.addEventListener('keydown', (evenement) => {
if (evenement.key === 'ArrowDown') options[0].focus();
}); C’est le réflexe le plus naturel, et c’est celui qui casse le composant.
Dès que le focus passe sur <li>, le champ n’est plus le champ : le curseur
de saisie n’y est plus, taper une lettre de plus ne filtre rien, et la touche
Trois variantes, plus discrètes :
aria-expandedposé une fois àtrueet jamais remis àfalse: le champ annonce « développé » alors que la liste est fermée, et l’on cherche une liste qui n’est plus là.- Un compteur affiché sans région de statut — « 3 résultats » écrit sous le champ. Qui ne voit pas l’écran continue de taper sans savoir qu’il a abouti.
- Un
<div>entre la liste et ses options :role="listbox"sur le<ul>,role="option"sur des<li>enveloppés dans autre chose. Le lien de propriété est rompu, le rang « 2 sur 5 » n’est plus annoncé.
Le code, pas à pas
1. D’abord un champ de formulaire ordinaire
<label for="theme">Thème du référentiel</label>
<input id="theme" type="text" /> Avant tout ARIA : c’est un champ, il lui faut une étiquette liée. Le placeholder ne compte pas — il s’efface dès la première lettre, et le champ se retrouve sans nom exactement au moment où l’on s’en sert.
2. Le champ déclare qu’il commande une liste
<input
id="theme"
type="text"
role="combobox"
aria-expanded="false"
aria-controls="theme-liste"
aria-autocomplete="list"
autocomplete="off"
/> role="combobox" se pose sur le champ, pas sur un conteneur autour : c’est
lui qui reçoit la saisie et commande la liste. aria-expanded dit si la liste
est ouverte, et doit suivre chaque ouverture et chaque fermeture. aria-controls désigne la liste gouvernée, aria-autocomplete="list" annonce
que les propositions arrivent dans une liste sans que le champ se complète tout
seul.
autocomplete="off" écarte l’historique du navigateur, qui recouvrirait la
liste. Attention : si le champ porte une donnée concernant l’utilisateur —
une adresse, une ville, un pays — le critère 11.13 attend au contraire un autocomplete pourvu d’une valeur pertinente. Sur ces champs-là, il faut choisir
la valeur juste (address-level2, country-name…) et vivre avec la superposition,
pas la supprimer.
3. La liste : listbox et option, sans rien entre les deux
<ul role="listbox" id="theme-liste" aria-label="Suggestions">
<li role="option" id="theme-option-0" aria-selected="false">Couleurs</li>
<li role="option" id="theme-option-1" aria-selected="false">Consultation</li>
</ul> Les options sont les enfants directs de la liste : un conteneur intercalé rompt
le lien de propriété, et le rang cesse d’être annoncé. Chaque option porte un id — c’est ce qui la rend désignable à l’étape suivante.
Le aria-label de la liste lui donne un nom. Sans lui, on entend « liste » sans
savoir de quoi.
4. aria-activedescendant : désigner sans déplacer le focus
champ.setAttribute('aria-activedescendant', options[actif].id);
options.forEach((option, rang) => {
option.setAttribute('aria-selected', String(rang === actif));
}); C’est l’attribut qui règle tout le problème. Le focus reste sur le champ ; le champ pointe vers l’option courante, et le lecteur d’écran l’annonce comme si elle avait le focus. La saisie continue de fonctionner pendant qu’on parcourt.
Deux conditions pour que cela marche : la valeur doit être l’id d’un élément réellement présent dans la liste gouvernée, et l’attribut doit être retiré
quand plus aucune option n’est désignée. Un aria-activedescendant qui pointe
vers un id disparu après filtrage n’annonce plus rien du tout.
L’état visuel suit la même désignation. Comme il ne s’agit pas d’un survol, distinguez l’option courante autrement que par la seule couleur de fond — la démo lui ajoute un liseré et une graisse.
5. Le nombre de suggestions, dans une région de statut
<!-- présente dès le premier rendu, et vide -->
<div role="status"></div> region.textContent =
suggestions.length === 0
? 'Aucune suggestion.'
: `${suggestions.length} suggestions disponibles, parcourez-les avec les flèches.`; La liste apparaît sans changement de page : rien n’avertit qui ne la voit pas.
Le critère 7.5 demande role="status" pour ce genre de message.
Deux précautions. La région doit exister dès le premier rendu, vide : créée au moment de l’annonce, elle n’est pas restituée par tous les lecteurs d’écran. Et elle ne porte que le compte — y verser les suggestions elles-mêmes ferait parler la région plusieurs secondes à chaque frappe, en couvrant l’écho de la saisie.
6. Le clavier, en entier
Tout se traite sur le champ, puisque c’est lui qui garde le focus. Le preventDefault() sur les flèches n’est pas facultatif : sans lui, la flèche
déplace aussi le curseur de saisie au début ou à la fin du texte.
7. <datalist>, quand il suffit
<label for="theme">Thème du référentiel</label>
<input id="theme" list="themes" />
<datalist id="themes">
<option value="Couleurs"></option>
<option value="Consultation"></option>
</datalist> Le navigateur donne alors la liste, le filtrage, le clavier et le lien avec le champ, sans une ligne de script. C’est la bonne réponse quand la liste est courte et que les suggestions sont de simples valeurs à recopier.
Ses limites sont réelles : l’apparence de la liste ne se met pas en forme, on ne choisit pas la façon dont le navigateur filtre, on n’y met pas de contenu riche, et la restitution varie d’un couple navigateur/lecteur d’écran à l’autre. Testez sur vos cibles avant de conclure — mais posez la question avant d’écrire les cent lignes du motif complet.
Au clavier
| Touche | Effet |
|---|---|
| Saisie de texte | Ouvre la liste et la filtre. Le focus reste dans le champ, toujours. |
| ↓ | Ouvre la liste, puis désigne l’option suivante — sans déplacer le focus. |
| ↑ | Désigne l’option précédente, puis revient au champ seul avant de boucler. |
| Entrée | Prend l’option désignée et referme la liste. Sans option désignée, valide le formulaire. |
| Échap | Ferme la liste en gardant la saisie. Liste déjà fermée, vide le champ. |
| Tab | Sort du champ. Le focus n’entre jamais dans la liste. |
Ce que le lecteur d’écran restitue
À la prise de focus : le nom du champ, sa nature et l’état de la liste — de l’ordre de « Thème du référentiel, zone d’édition combinée, réduit ». À la première frappe, la région de statut annonce « 3 suggestions disponibles ». Puis, à chaque flèche, l’option désignée et son rang : « Consultation, 2 sur 3 ». La formulation varie d’un lecteur à l’autre, pas les informations.
Si les flèches ne produisent rien, aria-activedescendant est absent ou pointe
vers un id que le filtrage vient de retirer. Si vous entendez « développé »
alors que la liste est fermée, aria-expanded n’est pas remis à jour. Si vous
entendez l’option sans son rang, un élément s’intercale entre la listbox et ses option. Et si le nombre de suggestions n’est jamais dit, la région de statut
manque — ou elle a été créée en même temps que le message.
Checklist
- Une étiquette liée au champ, jamais un placeholder en guise d’intitulé.
role="combobox"sur le champ, avecaria-expandedtenu à jour à chaque ouverture et fermeture.- Les
role="option"enfants directs durole="listbox", chacun avec unid. aria-activedescendantdésigne l’option courante, et le focus ne bouge pas.- Le nombre de suggestions dans une région
role="status"présente dès le départ. - Flèches avec
preventDefault(), Échap qui ferme sans effacer,mousedownempêché au clic. - Sur un champ portant une donnée de l’utilisateur, un
autocompletepertinent plutôt queoff. - Demandez-vous d’abord si
<datalist>ne suffirait pas.
Les critères RGAA en jeu
- Critère 7.1 — Compatibilité des scripts
Dès qu'on quitte <datalist> pour une liste construite au script, plus rien n'expose le composant : le rôle du champ, l'état ouvert ou fermé de la liste et l'option courante n'existent qu'à l'écran tant qu'on ne les déclare pas.
- Critère 7.3 — Contrôle des scripts au clavier
Les suggestions doivent se parcourir et se choisir au clavier — et sans que le focus quitte le champ, sous peine d'interrompre la saisie au moment où elle devait s'affiner.
- Critère 7.5 — Messages de statut
Le nombre de suggestions n'existe qu'à l'écran. Sans région de statut présente d'avance, personne ne sait que trois propositions viennent d'apparaître, ni qu'il n'y en a aucune.
- Critère 11.1 — Étiquette de champ de formulaire
Le champ d'une autocomplétion reste un champ de formulaire : le placeholder qui lui sert d'intitulé s'efface à la première frappe, et le champ se retrouve sans nom au moment où l'on s'en sert.
- Critère 11.13 — Remplissage automatique des champs
La recette du combobox pose autocomplete=off pour écarter le remplissage du navigateur ; sur un champ qui porte une donnée de l'utilisateur, cela retire précisément ce que le critère demande.
Vérifiez votre composant avec l'extension
Pour aller plus loin
- La liste déroulante : l’autre façon de choisir dans un ensemble connu, et celle que le navigateur tient tout seul.
- Le champ de recherche : le champ sur lequel se greffe le plus souvent une autocomplétion.
- Critère 7.5 — Messages de statut : ce que le référentiel attend d’une annonce qui n’accompagne aucun changement de page.
- « Message de statut » au glossaire : la
définition qui décide de ce qui doit passer par
role="status". - « Composant d’interface » au glossaire : ce qui fait relever un champ à suggestions du thème des scripts.
Sources
- ARIA APG — Combobox Pattern — le contrat de comportement
- ARIA APG — Editable Combobox With List Autocomplete — l’exemple dont ce guide est l’application
- MDN —
<datalist>— l’alternative native, quand elle suffit - WCAG — Understanding 4.1.2 Name, Role, Value — pourquoi le rôle et l’état doivent être exposés
- GOV.UK — accessible-autocomplete — une implémentation de référence, et ses arbitrages