English

Le laissez-passer A-38 — Partie 2 : Écouter la réponse

Table des matières

Écouter les réponses

Dans la partie précédente, tu as appris à poser les bonnes questions. Labels, types, autocomplete, regroupements. Ton formulaire sait maintenant demander les informations correctement.

Mais poser la question ne suffit pas. Il faut écouter la réponse.

Quand l'utilisateur se trompe de format, quand un champ obligatoire est vide, quand la saisie ne correspond pas à ce que tu attendais, c'est le moment critique. Soit ton formulaire guide, soit il punit. Soit il dit "ton email n'a pas le bon format, voici un exemple", soit il affiche un "Erreur" rouge sans explication et laisse l'utilisateur deviner.

Cet article couvre la deuxième moitié de la conversation : valider au bon moment, afficher des erreurs qui aident, récapituler les problèmes, et gérer les composants custom qui remplacent les éléments natifs.

Valider au bon moment

Le moment où tu valides un champ change tout à l'expérience.

Valider au chargement de la page, tous les champs obligatoires s'affichent en erreur avant que l'utilisateur ait tapé quoi que ce soit. C'est exactement ce que fait :invalid en CSS. Il s'applique dès que le DOM est construit, pas après l'interaction.

validation-timing.css
/* Se déclenche au chargement, l'utilisateur voit les erreurs immédiatement */
input:invalid {
  border-color: var(--color-error);
}
 
/* Se déclenche uniquement après interaction utilisateur */
input:user-invalid {
  border-color: var(--color-error);
}

:user-invalid ne se déclenche qu'après que l'utilisateur a interagi avec le champ. Une saisie, un effacement, ou une tentative de soumission. C'est la différence entre accueillir quelqu'un et l'agresser.

Mais le moment de la validation n'est pas qu'une question CSS. C'est une question de design de conversation :

  • Pendant la saisie (temps réel) : utile pour les formats stricts (email, téléphone), à condition de ne pas afficher l'erreur avant que l'utilisateur ait fini de taper. Un champ email qui crie "invalide" après la première lettre est hostile.
  • À la perte de focus (blur) : le champ est validé quand l'utilisateur passe au suivant. C'est souvent le meilleur compromis. Assez tôt pour corriger immédiatement, assez tard pour ne pas interrompre la saisie.
  • À la soumission : tous les champs sont validés d'un coup. Nécessaire comme filet de sécurité, mais insuffisant seul. L'utilisateur découvre 5 erreurs en même temps sans savoir par où commencer.

Le pattern le plus robuste combine les trois : :user-invalid en CSS pour le retour visuel immédiat, validation au blur pour les messages d'aide, et validation à la soumission comme filet final avec un résumé d'erreurs.

Des erreurs qui aident

Un message d'erreur a une seule mission : permettre à l'utilisateur de corriger. S'il ne dit pas quoi corriger et comment, il ne sert à rien.

"Erreur" ne dit rien. "Champ invalide" ne dit presque rien. "Ce champ attend une adresse email au format nom@domaine.fr" dit tout.

La mécanique repose sur deux attributs :

error-message.html
<label for="email">Adresse email</label>
<input
  id="email"
  type="email"
  required
  aria-describedby="email-error"
>
<p id="email-error" class="error-message"></p>
error-message.js
// Basculer l'état et remplir le message au moment de la validation
function setFieldError(field, message) {
  const errorNode = document.getElementById(`${field.id}-error`);
  field.setAttribute('aria-invalid', 'true');
  errorNode.textContent = message;
}
 
function clearFieldError(field) {
  const errorNode = document.getElementById(`${field.id}-error`);
  field.removeAttribute('aria-invalid');
  errorNode.textContent = '';
}
error-message.css
input:user-invalid {
  border-color: var(--color-error);
}
 
.error-message {
  color: var(--color-error);
  font-size: var(--font-size-sm);
}
 
.error-message:not(:empty)::before {
  content: "⚠";
  margin-inline-end: var(--space-1);
}

Deux pièges à éviter. D'abord, aria-invalid="true" ne doit pas être écrit en dur dans le HTML : copié tel quel, le champ serait signalé invalide dès le chargement. Il est posé dynamiquement, par JavaScript, au moment de la validation. Ensuite, pas d'aria-live="polite" sur le message lui-même : le message est déjà cible d'aria-describedby, et le résumé d'erreurs porte role="alert". Cumuler les deux sur le même élément expose à une double annonce. L'annonce vient du résumé (immédiate) et de la description (lue à la prise de focus sur le champ). Si tu tiens à une annonce immédiate lors d'une validation au blur, teste avec un lecteur d'écran avant de l'affirmer.

aria-invalid="true" dit à la technologie d'assistance que le champ contient une erreur. aria-describedby lie le champ au message qui explique l'erreur. Le lecteur d'écran annonce : "Adresse email, invalide, Ce champ attend une adresse email au format nom@domaine.fr."

Le signal visuel suit le même principe que les couleurs qui ne doivent jamais être seules. Bordure, texte et icône.

Quelques règles pour les messages d'erreur :

  • Dis ce qui ne va pas. "Saisissez votre numéro à 10 chiffres", pas "Ce champ est requis"
  • Dis comment corriger. "Format attendu : 06 12 34 56 78", pas "Format invalide"
  • Reste près du champ. Le message doit être visuellement adjacent au champ en erreur
  • Ne disparais pas trop vite. Le message reste visible tant que l'erreur n'est pas corrigée

Ces messages près des champs sont indispensables, mais ils ne suffisent pas seuls pour les formulaires longs. La section suivante couvre le résumé d'erreurs, la vue d'ensemble après soumission.

Le résumé d'erreurs

Les messages près des champs aident à corriger une erreur à la fois. Mais quand un formulaire de 10 champs est soumis avec 4 erreurs, l'utilisateur a besoin d'une vue d'ensemble. Surtout s'il ne voit pas toute la page.

Le résumé d'erreurs est un bloc qui apparaît en haut du formulaire après une soumission ratée. Il liste toutes les erreurs, avec un lien vers chaque champ concerné :

error-summary.html
<section role="alert" id="error-summary">
  <h2 id="error-summary-title" tabindex="-1">4 erreurs à corriger</h2>
  <ul>
    <li><a href="#email">Adresse email : ajoutez un @ et un domaine (ex: nom@domaine.fr)</a></li>
    <li><a href="#phone">Téléphone : saisissez votre numéro à 10 chiffres</a></li>
    <li><a href="#zip">Code postal : saisissez 5 chiffres (ex: 75001)</a></li>
    <li><a href="#terms">Conditions : cochez la case pour continuer</a></li>
  </ul>
</section>
error-summary.js
// Après une soumission échouée, envoyer le focus sur le titre du résumé
document.getElementById('error-summary-title').focus();

Trois choses se passent quand la soumission échoue :

  1. Le résumé apparaît en haut du formulaire avec la liste des erreurs
  2. Le focus est envoyé sur le titre du résumé (tabindex="-1" le rend focalisable programmatiquement) : le lecteur d'écran annonce "4 erreurs à corriger"
  3. Chaque erreur est un lien vers le champ concerné : un clic ou un Enter et l'utilisateur est sur le champ à corriger

Les deux mécanismes portent volontairement sur des noeuds différents. role="alert" est sur la <section>, qui contient le titre et la liste : lors d'une validation côté client, la région live est insérée dans la page, détecte le changement et annonce tout son contenu. Le focus, lui, va sur le <h2> : lors d'une validation côté serveur, le résumé est déjà présent au chargement, la région live n'a rien vu changer et reste muette, et c'est le focus qui annonce le titre. Chacun couvre le cas où l'autre se tait.

Ce n'est pas une précaution théorique. En posant role="alert" et tabindex="-1" sur le même élément, j'ai observé sous VoiceOver que Chrome lit le titre deux fois. Toujours sur un même élément, sous Safari, le focus programmé seul n'annonce rien : la seule annonce entendue provenait du rôle alerte, pas du focus. Le rôle alerte seul, lui, annonce dans les deux navigateurs. Ces observations se bornent à cette configuration, macOS avec VoiceOver, sur Safari et Chrome. Je n'ai pas encore testé sous NVDA ni JAWS, ni ce balisage-ci en combinant les deux mécanismes. GOV.UK sépare de la même manière, avec un conteneur intermédiaire ; ici le titre lui-même sert de cible de focus, ce qui évite un élément de plus. Le motif reste à vérifier au lecteur d'écran.

Le résumé ne remplace pas les messages près des champs : il les complète. L'utilisateur lit le résumé pour comprendre l'ampleur du problème, clique sur un lien pour aller au premier champ en erreur, corrige, puis utilise Tab pour trouver le champ suivant en erreur grâce au message adjacent.

Pour les formulaires courts (2-3 champs), le résumé est facultatif, les messages adjacents suffisent. Pour les formulaires longs, il devient indispensable.

Les composants custom

Le HTML natif offre <select>, <input type="date">, <input type="range">, <details>, <datalist>. Ils sont accessibles par défaut : navigables au clavier, annoncés correctement par les lecteurs d'écran, compatibles avec l'autocomplétion et la validation native.

Et pourtant, la majorité des projets les remplacent par des composants custom. Une <div> qui ressemble à un <select>. Un calendrier en JavaScript qui remplace <input type="date">. Une combobox faite maison qui réinvente <datalist>.

Pourquoi ? Parce que les composants natifs sont difficiles à styliser. Le <input type="date"> en est l'exemple le plus parlant : son popup est un widget natif du système d'exploitation, pas un élément web. En 2026, on ne peut toujours pas en changer les couleurs, la disposition ou la typographie. Son rendu varie entre Chrome, Firefox et Safari. Les pseudo-éléments WebKit ne fonctionnent pas partout. C'est une raison valable de le remplacer. Mais elle a un coût.

Un calendrier custom doit reproduire tout ce que le natif fait gratuitement :

  • Navigation clavier (Tab pour entrer, flèches pour se déplacer entre les jours, Entrée pour sélectionner, Échap pour fermer)
  • Annonce par le lecteur d'écran (rôle, date courante, mois affiché, jour sélectionné)
  • Mise à jour en temps réel (aria-selected, aria-current="date", aria-label sur chaque jour)
  • Gestion du focus (confinement dans le calendrier ouvert, retour au champ à la fermeture)

Voici la structure minimale d'un calendrier accessible, et c'est déjà conséquent :

custom-datepicker.html
<label for="departure">Date de départ</label>
<input
  id="departure"
  type="text"
  autocomplete="off"
  placeholder="jj/mm/aaaa"
>
<button
  aria-label="Choisir la date de départ"
  aria-haspopup="dialog"
  aria-expanded="false"
>📅</button>
 
<dialog aria-label="Choisir une date">
  <header>
    <button>Mois précédent</button>
    <h2 aria-live="polite">Avril 2026</h2>
    <button>Mois suivant</button>
  </header>
  <table role="grid" aria-label="Avril 2026">
    <thead>
      <tr>
        <th scope="col" aria-label="Lundi" title="Lundi">L</th>
        <th scope="col" aria-label="Mardi" title="Mardi">M</th>
        <th scope="col" aria-label="Mercredi" title="Mercredi">M</th>
        <th scope="col" aria-label="Jeudi" title="Jeudi">J</th>
        <th scope="col" aria-label="Vendredi" title="Vendredi">V</th>
        <th scope="col" aria-label="Samedi" title="Samedi">S</th>
        <th scope="col" aria-label="Dimanche" title="Dimanche">D</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td tabindex="-1" aria-label="1 avril 2026">1</td>
        <td tabindex="-1" aria-label="2 avril 2026">2</td>
        <!-- ... -->
        <td tabindex="0" aria-selected="true" aria-current="date" aria-label="22 avril 2026">22</td>
        <!-- ... -->
      </tr>
    </tbody>
  </table>
</dialog>

Et il manque encore tout le JavaScript : navigation au clavier entre les jours (flèches), entre les mois (Page Up/Down), entre les années (Maj + Page Up/Down), retour au champ à la sélection, fermeture à l'Échap, mise à jour de l'aria-label dynamique. Chaque comportement doit être implémenté manuellement. Le <input type="date"> natif le fait en zéro ligne de code.

Avant de construire un composant custom, pose-toi trois questions :

  1. Est-ce que le natif suffit ? Un <input type="date"> avec une bonne combinaison label + min/max + validation couvre souvent le besoin. Un <datalist> combiné à un <input> donne une combobox native.
  2. Est-ce que le design peut s'adapter ? Parfois, accepter les limites du natif coûte moins cher que maintenir un composant custom accessible.
  3. Si je n'ai pas le choix, est-ce que je suis le pattern ARIA ? Les ARIA Authoring Practices Guide documentent chaque pattern (combobox, dialog, grid, tabs, accordion) avec le clavier, les rôles et les états attendus. Ne pas les suivre, c'est réinventer la roue en carré.

Le sujet ARIA est couvert en deux articles : le poison et le scalpel. La règle d'or : si un élément HTML natif fait le travail, utilise-le.

Les critères d'ensemble

Le RGAA encadre la validation, les erreurs et les composants interactifs :

  • Critère 7.1 — chaque script est-il compatible avec les technologies d'assistance ? (motifs de conception ARIA)
  • Critère 7.3 — chaque script est-il contrôlable au clavier et par tout dispositif de pointage ?
  • Critère 7.5 — chaque message de statut est-il correctement restitué ? (role="alert" pour les erreurs)
  • Critère 11.9 — chaque intitulé de bouton est-il pertinent ? ("Envoyer" est vague, "Confirmer la commande" est explicite)
  • Critère 11.10 — le contrôle de saisie est-il utilisé de manière pertinente ?
  • Critère 11.11 — le contrôle de saisie est-il accompagné de suggestions de correction ?

La partie 3 couvrira le critère 11.12 (confirmation avant soumission pour les données financières ou légales) et les formulaires multi-étapes.

Sources