RGAA Lab

Composants accessibles

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-activedescendant compris.

Au clavier

ToucheEffet
TabDonne 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.
LettresSaute à la première option qui commence par les caractères tapés.
Début / FinVa à la première ou à la dernière option.
EntréeRetient l’option et referme la liste ; dans un formulaire, peut le soumettre.
ÉchapReferme 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

Sources