RGAA Lab

Composants accessibles

Le récapitulatif d'erreurs

Un formulaire long qui n'affiche que des messages inline laisse chercher : ni combien d'erreurs, ni où. Le résumé en tête, chaque entrée liée à son champ, et le focus qui s'y déplace. Démo, code, critères RGAA.

6 critères RGAA en jeu

Essayez-le

Deux nombres séparés par un point.

Adresse complète, protocole compris.

Focus : aucun élément de la démo n’a encore le focus

Envoyez le formulaire vide, sans toucher à la souris : le focus part au titre du récapitulatif — le témoin sous le formulaire lit le DOM et dit où il est vraiment allé. Tabulez jusqu’à une entrée de la liste, activez-la : le focus entre dans le champ concerné, qui se réannonce avec son état et son message. Corrigez un seul champ, renvoyez : le compte baisse, et c’est lui qui dit ce qu’il reste à faire.

Cette démo existe aussi en page autonome.

L’erreur courante

Un formulaire de douze champs, une soumission refusée, et rien d’autre que des messages posés à côté des champs fautifs.

<!-- À ne pas faire : rien en tête, trois erreurs dispersées dans douze champs -->
<form>
	<label for="page">Adresse de la page</label>
	<input id="page" aria-invalid="true" aria-describedby="page-erreur" />
	<p id="page-erreur">L'adresse est invalide.</p>

	<!-- … onze autres champs, dont deux en erreur … -->
</form>

Le balisage de chaque message est correct. Ce qui manque est ailleurs : après la soumission, personne ne sait qu’il s’est passé quelque chose. Le focus est resté sur le bouton d’envoi, ou est retombé en haut du document si la page s’est rechargée. Aucune annonce, aucun compte, aucune indication de direction.

Il faut alors parcourir le formulaire pour trouver les erreurs : champ par champ au lecteur d’écran, écran par écran à 400 % de zoom. Et rien ne dit quand on a fini de les trouver — la seule façon de le savoir est de renvoyer le formulaire.

Trois variantes coûtent presque autant :

  • Un récapitulatif sans liens — « 3 erreurs : critère, adresse, constat ». Il informe, puis abandonne : retrouver les champs reste à la charge de l’utilisateur, et c’était le plus long.
  • Un récapitulatif qui remplace les messages inline. La personne qui corrige le troisième champ a perdu de vue ce qu’on lui demandait, et doit remonter.
  • Le focus laissé sur le bouton d’envoi. Le résumé est inséré au-dessus : la tabulation avance et ne le rencontre jamais.

Le code, pas à pas

1. Le résumé, en tête de formulaire

<div class="recapitulatif">
	<h2 tabindex="-1">Il y a 2 problèmes</h2>

	<ul>
		<li><a href="#champ-critere">Le numéro s'écrit thème.critère, par exemple 11.10.</a></li>
		<li><a href="#champ-page">L'adresse doit commencer par https://</a></li>
	</ul>
</div>

<label for="champ-critere">Numéro de critère</label>
<input id="champ-critere"  />

Le résumé se place avant les champs dans le code source, pas seulement au-dessus à l’écran : c’est ce qui rend l’ordre de tabulation cohérent une fois le contenu inséré, et c’est le sujet du test 12.8.2.

Le texte du lien est le message d’erreur lui-même, pas « voir le champ » ni le nom du champ. La liste des liens de la page se lit alors toute seule, et l’utilisateur sait ce qui l’attend avant de sauter. C’est le choix de GOV.UK, et il tient.

Les entrées suivent l’ordre des champs, pas l’ordre où les erreurs ont été détectées : la personne corrige de haut en bas.

2. Le lien mène au champ, pas à son étiquette

Un lien d’ancre vers un élément focalisable y place le focus — c’est le comportement natif du navigateur, sans une ligne de JavaScript. Encore faut-il que la cible soit le champ.

<!-- Le focus arrive dans le champ -->
<a href="#champ-page"></a>
<input id="champ-page"  />

<!-- À ne pas faire : la page défile, le focus ne bouge pas -->
<a href="#etiquette-page"></a>
<label id="etiquette-page" for="champ-page">Adresse de la page</label>

Deux cas demandent un choix. Pour un groupe de boutons radio ou de cases à cocher, visez le premier contrôle du groupe : c’est lui qui prend le focus, et la legend du fieldset sera lue avec. Pour un composant sur mesure, visez l’élément qui reçoit réellement le focus, pas son conteneur.

3. Le message reste aussi à côté du champ

<label for="champ-page">Adresse de la page</label>
<p class="aide" id="aide-page">Adresse complète, protocole compris.</p>
<p class="erreur" id="erreur-page">
	<span aria-hidden="true"></span> L'adresse doit commencer par https://
</p>

<input
	id="champ-page"
	aria-describedby="aide-page erreur-page"
	aria-invalid="true"
/>

Le référentiel se lit ici de près. Le test 11.10.3 accepte deux conditions au choix : un message visible qui identifie nommément le champ, ou un champ qui porte aria-invalid="true". Un récapitulatif qui nomme les champs satisfait donc la première, à lui seul.

Mais posez aria-invalid — le référentiel vous laisse le choix, l’état du champ mérite d’être exposé quand même — et le test 11.10.4 s’applique à son tour : le message doit alors être dans l’étiquette associée au champ, ou dans le passage de texte qui lui est associé. C’est ce que fait aria-describedby. Le récapitulatif seul ne suffit plus.

Notez ce que le RGAA demande exactement : un lien programmatique, pas une proximité visuelle. Un message situé en haut du formulaire et rattaché par aria-describedby satisfait le test. La proximité, elle, sert autre chose : à 400 % de zoom, ou sur un écran de téléphone, le champ et son message doivent tenir ensemble sous les yeux. C’est une bonne pratique solide, pas une exigence du référentiel.

4. Le focus va au récapitulatif

formulaire.prepend(recapitulatif);
recapitulatif.querySelector('h2').focus(); // le titre porte tabindex="-1"

tabindex="-1", jamais 0 : le titre doit pouvoir recevoir le focus par script, sans devenir un arrêt permanent de tabulation.

Le focus va sur le titre plutôt que sur le conteneur, pour une raison pratique : un titre focalisé s’annonce avec son texte et son niveau, et la liste des erreurs suit immédiatement dans l’ordre de lecture. Un div générique focalisé donne un résultat qui varie d’un lecteur d’écran à l’autre ; si vous tenez à le focaliser, nommez-le (role="group" et aria-labelledby vers le titre).

Et pour un formulaire court ? Deux champs, une erreur : poser le focus directement sur le champ fautif est plus rapide, et c’est ce que décrit la fiche des champs de formulaire. Le récapitulatif gagne sa place quand le formulaire ne tient plus sur un écran.

5. Ni role="alert" ni aria-live — puisque le focus s’y déplace

Le glossaire du référentiel définit le message de statut ainsi : « Un message de statut informe l’utilisateur d’un changement de contenu dans la page sans interrompre son activité principale (il n’y a pas de changement de contexte par exemple un repositionnement du focus sur le message). »

La conséquence est nette, et elle sépare deux implémentations :

  • Le récapitulatif prend le focus — il n’est pas un message de statut, et le critère 7.5 ne s’y applique pas. Pas de role="alert", pas de aria-live : le déplacement du focus provoque déjà la restitution, et cumuler les deux fait annoncer la même chose deux fois.
  • Le récapitulatif ne prend pas le focus — par exemple un compteur d’erreurs qui se met à jour pendant la saisie. C’est alors un message de statut qui avertit d’une erreur, et le test 7.5.2 demande role="alert".

La confirmation de succès, elle, ne prend jamais le focus : elle n’appelle aucune action. role="status" (test 7.5.1), sur une région présente dès le premier rendu, vide — créée au moment de l’annonce, elle n’est pas restituée.

<!-- Présent au chargement, rempli plus tard -->
<p role="status"></p>

6. Le compte, et le titre de la page

« Il y a 3 problèmes » plutôt que « Erreurs de saisie » : le nombre est l’information utile, parce qu’il change entre deux soumissions. Il dit la progression sans qu’on ait à la mesurer. Accordez le singulier — « Il y a un problème » — plutôt que d’écrire « 1 problème(s) ».

Ce titre est un titre au sens du critère 9.1 : il se balise en <hx>, à un niveau cohérent avec la page. Un lecteur d’écran peut alors y revenir par la navigation par titres, depuis n’importe quel champ, sans tabuler à l’envers.

Si la soumission recharge la page, préfixez aussi son <title> : « Erreur — Déclarer une non-conformité ». C’est la première chose annoncée au chargement.

7. Le même résumé, mais avant l’envoi

Tout ce qui précède se déclenche sur un refus. Reste ce que le contrôle de saisie ne voit pas : une valeur valide et fausse quand même — le mauvais numéro de critère, le mauvais montant, la mauvaise page supprimée.

Le critère 11.12 vise ces formulaires-là : ceux qui modifient ou suppriment des données, transmettent des réponses à un test, ou engagent financièrement ou juridiquement. Il laisse trois portes au choix — pouvoir modifier ou annuler après la validation, pouvoir vérifier et corriger avant la validation d’un formulaire en plusieurs étapes, ou un mécanisme de confirmation explicite (case à cocher ou étape supplémentaire). La deuxième est le récapitulatif d’erreurs retourné : la même liste, avant l’envoi, valeur par valeur.

<h1>Vérifiez votre déclaration</h1>

<dl>
	<div>
		<dt>Numéro de critère</dt>
		<dd>11.10</dd>
		<dd><a href="/declarer/critere">Modifier<span class="sr-only"> le numéro de critère</span></a></dd>
	</div>
	<!-- … une ligne par donnée saisie … -->
</dl>

Deux différences avec le récapitulatif d’erreurs, et elles se déduisent des sections précédentes :

  • Rien ne prend le focus, rien n’est annoncé. Cet écran est la page, pas un contenu inséré dans une page déjà lue : ni tabindex="-1", ni role="alert". Le chargement l’annonce, son <h1> le nomme.
  • Le lien porte le nom de la donnée, comme l’entrée du résumé portait le message. Douze « Modifier » identiques donnent douze liens indistinguables dans la liste des liens ; le complément peut rester masqué, la colonne dit déjà à l’œil de quoi il s’agit.

Un écran qui se contente d’afficher les valeurs, sans retour possible vers les champs, n’ouvre pas la deuxième porte — vérifier et corriger avant la validation. Il peut encore valoir comme étape supplémentaire de confirmation, mais il informe, comme le récapitulatif sans liens plus haut, puis abandonne. Et le champ modifié doit ramener ici, pas à l’étape suivante — sinon corriger une ligne fait retraverser le formulaire entier.

Au clavier

ToucheEffet
Entrée (dans un champ)Soumet. En cas d’erreur, le focus va au titre du récapitulatif, pas au premier champ.
Tab (depuis le titre)Descend dans la liste des erreurs, entrée par entrée.
Entrée (sur une entrée)Place le focus dans le champ fautif, qui se réannonce avec son message.
Tab (depuis la dernière entrée)Entre dans le formulaire au premier champ : le résumé est avant eux dans le code.
Maj + Tab (depuis le premier champ)Revient au récapitulatif, et nulle part ailleurs.

Ce que le lecteur d’écran restitue

À la soumission, le focus déplacé provoque l’annonce du titre, puis la liste se lit à la suite : « Il y a 2 problèmes, titre de niveau 2 », « liste de 2 éléments », « Le numéro s’écrit thème.critère, par exemple 11.10, lien ». En activant l’entrée : « Numéro de critère, champ de saisie, invalide, deux nombres séparés par un point ». La formulation varie d’un lecteur à l’autre ; les informations, non.

Le diagnostic inverse tient en quatre constats :

  • Rien n’est annoncé après la soumission : le focus n’a pas été déplacé, et aucun role="alert" ne prend le relais — le récapitulatif s’affiche pour l’œil seul.
  • Tout est annoncé deux fois : le focus déplacé et role="alert", cumulés.
  • Le champ ne dit pas « invalide » : aria-invalid manque, ou n’a pas été remis à jour après correction.
  • Le champ ne relit pas son message : aria-describedby ne le désigne pas, ou désigne un identifiant absent du document.

Checklist

  • Un récapitulatif en tête de formulaire dès qu’une soumission échoue, avec le nombre d’erreurs.
  • Le nombre dans un <hx> de niveau cohérent, focalisé par tabindex="-1".
  • Une entrée par erreur, dans l’ordre des champs, chacune un lien vers le champ — pas vers son étiquette.
  • Le texte du lien est le message d’erreur lui-même, celui qui dit quoi écrire.
  • Le message aussi à côté du champ, lié par aria-describedby, le champ portant aria-invalid="true".
  • Ni role="alert" ni aria-live sur un récapitulatif qui prend le focus.
  • role="status", présent et vide dès le chargement, pour la confirmation de succès.
  • Sur un formulaire qui engage : un écran de vérification avant l’envoi, chaque ligne ramenant à son champ.

Les critères RGAA en jeu

  • Critère 11.10 — Contrôle de saisie

    Le test 11.10.3 se contente d'une condition sur deux, et le récapitulatif ne couvre pas la première : ses entrées portent le message, pas le nom du champ — c'est aria-invalid qui tient, et il fait aussitôt entrer le test 11.10.4, qui veut le message dans l'étiquette ou dans le passage de texte associé au champ.

  • Critère 11.11 — Suggestions de correction de saisie

    Le texte d'une entrée du récapitulatif est celui du message d'erreur : s'il se contente de « Champ invalide », le résumé recopie l'inutile et la personne n'apprend toujours pas quoi écrire.

  • Critère 7.5 — Messages de statut

    Deux implémentations existent, et une seule est juste à la fois : le récapitulatif qui prend le focus n'est pas un message de statut, celui qui ne le prend pas en est un et doit porter role="alert". Cumuler les deux fait annoncer deux fois.

  • Critère 12.8 — Cohérence de l’ordre de tabulation

    Le résumé est inséré en haut du formulaire après coup, et le focus y est déplacé : le contenu ajouté doit se placer avant les champs dans le code, sinon la tabulation revient en arrière ou ne le rencontre jamais.

  • Critère 9.1 — Structure par les titres

    « Il y a 3 problèmes » est un titre : structuré en hx à un niveau cohérent, il donne un point de retour par la navigation par titres, ce qu'un paragraphe en gras ne donne pas.

  • Critère 11.12 — Modification des données saisies

    Le récapitulatif ne paraît que si la saisie est refusée : sur un formulaire qui engage — un paiement, une suppression, une réponse d'examen —, une valeur valide mais fausse part sans un mot, et ce qui manque n'est pas un résumé d'erreurs mais l'écran de vérification avant l'envoi.

Vérifiez votre composant avec l'extension

Pour aller plus loin

Sources