La liste déroulante
Le select natif donne le clavier, le tactile et le sélecteur du système sans qu'on écrive une ligne. Quand il suffit, ce que coûte de le refaire, et comment grouper ses options. Démo, code, critères RGAA.
5 critères RGAA en jeu
Essayez-la
Ouvrez la liste au clavier et descendez : le relevé sous la liste est pris dans
le DOM à chaque changement, et montre le nom du groupe dès que l’option choisie
appartient à un <optgroup>. Tapez maintenant 1 ou 7 liste fermée : le
navigateur saute à la première option qui commence par ce caractère — un
comportement que personne n’a écrit ici.
Cette démo existe aussi en page autonome.
L’erreur courante
Le select refait à la main, parce que la flèche du navigateur ne se style pas.
<!-- À ne pas faire -->
<div class="select" tabindex="0" role="listbox" aria-expanded="false">
<span class="valeur">Choisissez un critère</span>
<ul class="options">
<li role="option" onclick="choisir('7.3')">7.3 — Contrôle au clavier</li>
<li role="option" onclick="choisir('11.1')">11.1 — Étiquette de champ</li>
</ul>
</div> Ce composant a l’air juste : il a un rôle, des options, un état. Il est cassé de quatre façons.
Il n’a pas de nom. Un <label for> ne s’attache qu’à un champ de
formulaire, pas à un div : le composant est annoncé « zone de liste » et rien
d’autre. Il faudrait un aria-labelledby explicite, que personne n’ajoute
puisque le <label> est resté à côté et semble faire son travail.
Son rôle ne dit pas ce que fait le composant. Le div est à la fois le
déclencheur et la liste ; le motif d’un contrôle qui se déplie sur une liste est
le combobox — un élément qui porte le nom, l’état déplié et l’aria-controls,
et une listbox séparée qui porte les options. Ici, le seul role="listbox" annonce « zone de liste » sans jamais dire qu’il y a quelque chose à ouvrir, et
il demande bien plus que deux attributs.
Ses options ne sont pas ses options. La <ul> intercalée entre le role="listbox" et les role="option" rompt la filiation attendue : le rang
(« 3 sur 7 ») n’est plus calculable, et aucun aria-selected ne dit laquelle
est retenue.
Le tabindex="0" donne le focus, et s’arrête là. Pas de flèches, pas de
saisie des premières lettres, pas de Début ni de Fin, pas
d’Échap pour refermer sans changer la valeur. Et sur un téléphone,
le <select> natif ouvre le sélecteur du système — une roue sur iOS, une
liste plein écran sur Android — que ce div ne reproduit pas.
Trois variantes plus discrètes, sur de vrais <select> cette fois :
- La première option en guise d’étiquette (
<option>Pays</option>sans<label>) : le nom accessible du champ n’existe pas, et le texte qui en tenait lieu disparaît de l’écran dès qu’on choisit. - La navigation au changement de valeur (
onchange="location.href = …") : au clavier, les flèches parcourent les options une à une, et la page part sur la première rencontrée. - Les options-séparateurs (
<option disabled>— Europe —</option>) : elles imitent le rendu d’un<optgroup>sans en porter la sémantique, et rien n’annonce le groupe auquel appartient l’option lue.
Le code, pas à pas
1. Un <select>, et un <label for>
<label for="critere">Critère à ouvrir</label>
<select id="critere" name="critere">
<option value="7.3">7.3 — Contrôle au clavier</option>
<option value="11.1">11.1 — Étiquette de champ</option>
</select> Le référentiel range explicitement <select>, <optgroup> et <option> parmi
les champs de formulaire : ce qui vaut pour un champ texte vaut ici, à commencer
par l’étiquette liée. Le test 11.1.1 admet cinq façons de donner une étiquette
(aria-labelledby, aria-label, <label for>, title, bouton adjacent) ; dès
qu’on prend le <label for>, le test 11.1.2 exige que la valeur du for soit
celle de l’id du champ. C’est aussi la seule des cinq qui agrandit la zone de
clic — cliquer l’étiquette donne le focus à la liste.
Ce que le navigateur ajoute sans qu’on l’écrive : le focus, les flèches, la
saisie des premières lettres, Début et Fin, la fermeture
par Échap, le sélecteur natif sur mobile, l’envoi avec le formulaire
et le remplissage automatique. C’est la liste de tout ce qu’il faudrait réécrire
pour repartir d’un div — et de tout ce qu’on oublie en le faisant.
2. La première option, vide, qui ne remplace pas l’étiquette
<select id="critere" name="critere">
<option value="">Choisissez un critère</option>
…
</select> Deux choses distinctes, souvent confondues.
Le value="" sert à distinguer « je n’ai pas choisi » de « j’ai choisi la
première entrée ». Sans lui, un formulaire part avec une valeur que personne n’a
retenue, et le champ obligatoire ne peut plus se contrôler.
Mais ce texte n’est pas une étiquette. Il n’est pas le nom accessible du
champ, il n’est plus visible dès qu’on a choisi, et il ne dit rien quand on
revient sur le formulaire à moitié rempli. Le <label> reste nécessaire, en
plus.
3. <optgroup> quand les options font des familles
<select id="critere" name="critere">
<option value="">Choisissez un critère</option>
<optgroup label="Scripts">
<option value="7.1">7.1 — Compatibilité des scripts</option>
<option value="7.3">7.3 — Contrôle au clavier</option>
</optgroup>
<optgroup label="Formulaires">
<option value="11.1">11.1 — Étiquette de champ</option>
</optgroup>
</select> Le label de l’<optgroup> est obligatoire, et il est lu avant l’option :
« Scripts, 7.3 — Contrôle au clavier ». C’est ce qui remplace, à l’oreille,
le décrochage visuel qu’on croit obtenir avec une ligne de tirets.
Le critère 11.8 dit « si nécessaire » : une liste de six entrées n’a rien à regrouper, deux cents départements si. Le regroupement s’impose quand les options se rangent d’elles-mêmes en familles — c’est un jugement à porter, pas une case à cocher.
Deux limites de l’élément : les <optgroup> ne s’imbriquent pas, et un <optgroup> ne se choisit pas. Une hiérarchie à trois niveaux n’est pas une
liste déroulante.
4. Choisir n’est pas valider
<label for="critere">Critère à ouvrir</label>
<select id="critere" name="critere">…</select>
<button type="submit">Afficher le critère</button> Le « menu de saut » — onchange qui change de page — échoue par construction au
clavier. Sur une liste fermée, la flèche bas ne prévisualise pas : elle change
la valeur, déclenche change, et la navigation part sur l’option suivante alors
qu’on voulait descendre jusqu’à la dixième.
Le critère 7.4 admet trois façons d’initier un changement de contexte : un bouton explicite, un lien explicite, ou un texte qui prévient avant le déclenchement. Le bouton est la plus simple des trois, et la seule qui laisse parcourir la liste tranquillement.
5. multiple, presque jamais
<!-- Techniquement conforme, et presque personne n'y arrive -->
<select id="themes" name="themes" multiple size="6">…</select> Rien dans ce contrôle n’indique qu’il accepte plusieurs valeurs. À la souris, il faut connaître Ctrl ou Cmd + clic, et un clic simple efface toute la sélection précédente. Au clavier, il faut Ctrl + flèches pour se déplacer sans désélectionner, puis Espace — une combinaison que rien n’enseigne.
Le RGAA ne l’interdit pas : c’est un jugement d’utilisabilité, pas une exigence.
Mais un <fieldset> de cases à cocher montre les choix, dit qu’ils sont
cumulables, et se manipule sans rien connaître.
6. Et parfois, ce n’est pas une liste déroulante
- Moins de cinq options, et le choix compte ? Des boutons radio les montrent toutes en même temps, et se choisissent d’un clic au lieu de deux.
- Deux états ? Une case à cocher.
- Des centaines d’entrées à filtrer en tapant ? C’est un combobox, et
c’est le seul cas où refaire le composant se justifie — au prix du motif
complet de l’ARIA APG,
aria-activedescendantcompris.
Au clavier
| Touche | Effet |
|---|---|
| Tab | Donne le focus à la liste — un seul arrêt, quel que soit le nombre d’options. |
| ↑ ↓ | Change de valeur, ou déplace dans la liste ouverte. Le focus reste sur la liste. |
| Lettres | Saute à la première option qui commence par les caractères tapés. |
| Début / Fin | Va à la première ou à la dernière option. |
| Entrée | Retient l’option et referme la liste ; dans un formulaire, peut le soumettre. |
| Échap | Referme la liste ouverte sans changer la valeur. |
Le détail varie selon le système et le navigateur : sur une liste fermée, la flèche bas change la valeur ici et ouvre la liste là. C’est justement ce que l’élément natif prend en charge, et ce qu’un composant refait devrait réécrire pour chaque plateforme.
Ce que le lecteur d’écran restitue
À la prise de focus : le nom, la nature, la valeur et le rang — de l’ordre de « Critère à ouvrir, zone de liste déroulante, Choisissez un critère, 1 sur 7 ». En descendant dans un groupe, le nom du groupe précède l’option : « Scripts, 7.3 — Contrôle au clavier, 5 sur 7 ». La formulation change d’un lecteur à l’autre ; les informations, non.
Si vous n’entendez pas « Critère à ouvrir », le for ne désigne pas l’id du
champ — ou il n’y a pas de <label>. Si le nom que vous entendez est
« Choisissez un critère », c’est la première option qui sert d’étiquette, et le
champ n’en a pas. Si vous n’entendez jamais de nom de groupe, les familles sont
faites d’options désactivées et non d’<optgroup>. Et si vous n’entendez pas de
rang, vous êtes devant un faux select.
Checklist
- Un
<select>natif, sauf besoin qu’il ne couvre pas. - Un
<label for>lié par l’id— jamais la première option en guise d’étiquette. - Une première option à
value=""quand rien n’est présélectionné. - Des
<optgroup label="…">dès que les options forment des familles ; jamais d’options-séparateurs. - Choisir ne navigue pas : c’est un bouton qui valide.
- Avant
multiple, essayez un<fieldset>de cases à cocher.
Les critères RGAA en jeu
- Critère 11.1 — Étiquette de champ de formulaire
Un select est un champ de formulaire au sens du référentiel : il lui faut une étiquette liée. La première option employée en guise d'intitulé disparaît dès qu'on choisit, et le champ reste sans nom.
- Critère 11.8 — Items des listes de choix regroupés
Dès que les options forment des familles, elles se regroupent en optgroup portant un label. Les fausses options-séparateurs en imitent le rendu sans rien annoncer du groupe.
- Critère 7.1 — Compatibilité des scripts
Un select refait en div doit exposer lui-même son nom, son rôle, sa valeur et ses changements d'état — ce que l'élément natif donne, et qu'un role listbox mal câblé ne rend pas.
- Critère 7.3 — Contrôle des scripts au clavier
Flèches, saisie des premières lettres, Début et Fin, Échap : le select natif tient tout cela. Un div avec tabindex ne prend que le focus, et rien de ce qu'on en attend ensuite.
- Critère 7.4 — Changement de contexte annoncé
La liste qui navigue dès que la valeur change part, au clavier, sur la première option parcourue. Le référentiel admet trois issues — avertir avant, ou faire déclencher le changement par un bouton ou un lien explicite — et le bouton est la plus simple.
Vérifiez votre composant avec l'extension
Pour aller plus loin
- Les champs de formulaire : l’étiquette, l’aide et l’erreur, qui valent pour la liste déroulante comme pour le reste.
- Critère 11.8 — Items des listes de choix regroupés : ce que
l’audit vérifie sur les
<optgroup>, et ce qu’il laisse à votre jugement. - Critère 7.4 — Changement de contexte annoncé : pourquoi le menu de saut échoue, et ce qui le corrige.
- « Items de même nature d’une liste de choix » au glossaire : la définition qui décide de ce qu’on regroupe.
Sources
- MDN —
<select>— l’élément et tout ce qu’il apporte - MDN —
<optgroup>— le regroupement des options - ARIA APG — Combobox Pattern — ce qu’il faut écrire quand le natif ne suffit plus
- WCAG — Understanding 3.2.2 On Input — pourquoi choisir ne doit pas déclencher
- GOV.UK Design System — Select — pourquoi ils recommandent de s’en passer quand c’est possible