RGAA Lab

Composants accessibles

Le champ de recherche

Un placeholder n'est pas une étiquette, une loupe n'est pas un nom. Le champ de recherche est le cas où le référentiel a prévu qu'aucune étiquette ne soit visible — à des conditions précises, qu'il faut lire. Démo, code, critères RGAA.

6 critères RGAA en jeu

Essayez-le

Tapez un mot et validez avec Entrée depuis le champ : c’est le <form> qui donne ce raccourci, et le nombre de résultats est annoncé sans qu’on ait à le chercher. Le champ n’affiche aucune étiquette — le second bouton inspecte le formulaire réel et montre d’où vient tout de même son nom.

Cette démo existe aussi en page autonome.

L’erreur courante

Un champ étiqueté par son seul placeholder, et un bouton qui ne porte qu’une loupe.

<!-- À ne pas faire -->
<div class="recherche">
	<input type="text" placeholder="Rechercher" />
	<button type="submit"><img src="/img/loupe.svg" /></button>
</div>

Trois pertes, dans trois registres différents.

Le champ n’a pas d’étiquette. À l’arrivée du focus, on entend bien « Rechercher, zone de saisie » : le placeholder sert de dernier recours au calcul du nom accessible. Mais il disparaît dès la première frappe — le nom s’évapore au moment où l’on en aurait besoin pour se relire — et le référentiel ne le compte pas parmi les façons d’étiqueter un champ (glossaire « Étiquette de champ de formulaire »).

Le bouton n’a pas de nom non plus. L’image sans alt ne fournit aucun intitulé ; selon le navigateur, on entend « bouton » seul, ou le nom du fichier. La fonction la plus importante du composant est devenue anonyme.

La zone n’existe pas pour la navigation par repères. Un div ne désigne rien : la seule façon d’atteindre la recherche est de tabuler jusqu’à elle, depuis le début de la page, à chaque page.

Trois variantes, plus discrètes :

  • aria-label="Rechercher" sur le champ, avec un bouton en loupe seule. Le nom existe, mais plus aucun texte n’est visible près du champ : le test 11.1.3 réclame alors un title, ou un passage de texte visible accolé au champ, ou un passage qui devient visible à la prise de focus.
  • Un div role="search" autour d’un champ et d’un bouton, sans <form>. Entrée dans le champ ne lance plus rien : il faut atteindre le bouton pour chercher.
  • Le champ replié derrière une icône sur les pages intérieures, déplié en en-tête sur l’accueil. La fonctionnalité change de nature d’une page à l’autre — c’est exactement ce que le critère 12.5 refuse.

Le code, pas à pas

1. Un formulaire, et un repère

<form role="search" action="/recherche"></form>

Le formulaire est la zone de recherche : il n’y a pas de conteneur à ajouter autour. role="search" le fait apparaître dans la liste des repères, celle qu’un lecteur d’écran parcourt d’une touche.

L’élément natif <search> produit le même repère sans attribut. Il est récent ; le rôle explicite est celui que les technologies d’assistance connaissent depuis le plus longtemps. Choisissez l’un ou l’autre : les deux ensemble font doublon.

Le critère 12.6 n’impose pas ce repère. Il accepte aussi un titre qui nomme la zone, un lien d’évitement, un lien d’accès rapide ou un bouton de masquage. Le repère est simplement celui qui ne coûte qu’un attribut.

S’il y a deux zones de recherche dans la page — le moteur du site et celle d’un catalogue —, nommez-les : aria-label="Rechercher dans le site". Deux repères identiques ne se distinguent pas dans une liste.

2. L’étiquette, visible ou non

Le cas par défaut reste le plus simple :

<label for="recherche">Rechercher dans le site</label>
<input id="recherche" type="search" />

Mais un champ de recherche affiche rarement son étiquette, et le référentiel a prévu ce cas. Le test 11.1.1 accepte, parmi ses conditions, celle-ci :

Un bouton adjacent au champ de formulaire lui fournit une étiquette visible et un élément <label> visuellement caché ou un attribut WAI-ARIA aria-label, aria-labelledby ou title lui fournit un nom accessible.

Deux choses, donc, et pas une seule :

<form role="search">
	<label for="recherche" class="sr-only">Rechercher dans le site</label>
	<input id="recherche" type="search" />

	<button type="submit">Rechercher</button>
</form>

Le bouton porte le texte visible qui dit à l’œil ce qu’est ce champ, et il est adjacent — voisin immédiat dans le rendu et dans le code source, comme le glossaire le définit. Le <label> masqué porte le nom accessible, que la classe sr-only retire de l’affichage sans le retirer de l’arbre.

Retirez le texte du bouton, et la condition tombe : il ne reste plus rien de visible, et le test 11.1.3 demande alors un title sur le champ ou un texte accolé. C’est le lien direct entre les deux erreurs les plus courantes de ce composant.

3. type="search", pour ce qu’il donne vraiment

<input id="recherche" type="search" name="q" />

Il apporte le contrôle d’effacement des navigateurs qui en proposent un, et un clavier virtuel dont la touche de validation porte « rechercher ». Il ne fournit aucun nom : le type n’est pas une étiquette, et le champ sans <label> reste sans nom quel que soit son type.

4. Le bouton, même s’il ne porte qu’une loupe

<button type="submit">
	<svg aria-hidden="true" focusable="false"></svg>
	Rechercher
</button>

La loupe est un doublon visuel du mot : masquée par aria-hidden, elle n’a rien à annoncer en plus. Le texte, lui, sert deux fois — comme intitulé du bouton (critère 11.9) et comme étiquette visible du champ (critère 11.1).

Si la place manque vraiment et que le bouton se réduit à l’icône, le nom se donne autrement — et il faut alors régler la question de l’étiquette visible, vue au point 2 :

<button type="submit">
	<svg aria-hidden="true" focusable="false"></svg>
	<span class="sr-only">Rechercher</span>
</button>

Le texte masqué se préfère à un aria-label : il est traduit par les outils de traduction automatique, ce qu’un attribut n’est pas toujours. Les deux satisfont le test 11.9.1, qui accepte le contenu du <button>, l’aria-label, l’aria-labelledby, le value d’un <input>, l’alt d’un bouton image ou le title.

5. Les résultats s’annoncent

Quand la recherche s’affiche sans changer de page, le compte est un message de statut :

<!-- Présente dès le chargement, vide, et remplie plus tard -->
<p role="status"></p>

La région doit exister avant l’annonce. Créée au moment où le résultat arrive, elle n’est pas restituée par tous les lecteurs d’écran — et l’on obtient alors une recherche qui semble ne rien faire.

Annoncez le nombre, pas la liste : « 3 résultats pour « onglets » » suffit à savoir qu’il faut aller lire, et une liste entière lue à voix haute dans une région d’annonce est ingérable.

6. Le même moteur, à la même place

Le critère 12.5 porte sur l’ensemble des pages, pas sur une seule : la même fonctionnalité, à la même place dans la présentation, dans le même ordre relatif dans le code source. C’est le critère qu’on casse en refactorant un en-tête, et que rien ne signale sur la page qu’on regarde.

Au clavier

ToucheEffet
TabEntre dans le champ, puis va au bouton de soumission.
EntréeDepuis le champ, soumet le formulaire — comportement du <form>, qu’un div ne donne pas.
Entrée / EspaceSur le bouton, lance la recherche.
Navigation par repèresAtteint la zone de recherche sans tabuler depuis le haut de la page (D sous NVDA, R sous JAWS).

Ce que le lecteur d’écran restitue

À l’entrée dans la zone, puis dans le champ : le repère, le nom, la nature — de l’ordre de « recherche, repère — Rechercher dans le site, champ de recherche ». Sur le bouton : « Rechercher, bouton ». La formulation varie d’un lecteur à l’autre, les informations non.

Si le nom que vous entendez disparaît dès que vous tapez une lettre, c’est le placeholder qui le fournissait : ce n’est pas une étiquette. Et si vous n’entendez que « zone de saisie, vide », rien du tout ne nomme le champ. Si vous n’entendez que « bouton », la loupe n’a pas d’alternative textuelle. Et si la liste des repères de la page ne contient pas « recherche », c’est que le role="search" ou l’élément <search> manque.

Checklist

  • Un <form> — pas un div — porteur du repère de recherche, nommé s’il y en a deux.
  • Une <label> liée au champ ; le placeholder reste un exemple, jamais l’étiquette.
  • Étiquette masquée : un texte visible accolé, celui du bouton, ou à défaut un title.
  • Le bouton nommé même réduit à une loupe ; le glyphe en aria-hidden="true".
  • type="search" sur le champ, pour l’effacement et le clavier mobile.
  • Le nombre de résultats dans une région role="status" présente dès le chargement.
  • Le même moteur, à la même place, dans le même ordre du code, sur toutes les pages.

Les critères RGAA en jeu

Vérifiez votre composant avec l'extension

Pour aller plus loin

Sources