Le formulaire d'inscription, vu sans les yeux
Imagine ton formulaire d'inscription. À la souris, tout va bien : tu cliques sur le champ, tu vois l'étiquette juste au-dessus, tu tapes. Le geste est évident.
Maintenant ferme les yeux. Marie, qui utilise un lecteur d'écran, n'a pas cette étiquette sous les yeux. Son logiciel arrive sur le champ et annonce... « champ de saisie ». De quoi ? Mystère. Nom, email, mot de passe ? Aucune idée. Elle tape à l'aveugle, valide, et le formulaire la renvoie en erreur. Elle abandonne.
Le formulaire est l'endroit où l'accessibilité se joue le plus, parce que c'est là que l'utilisateur doit agir, pas seulement lire. Et la bonne nouvelle, comme toujours, c'est que presque tout se règle avec le bon HTML. Cette leçon te donne les trois mécanismes qui transforment un formulaire muet en un formulaire qui se laisse remplir sans les yeux : le label, les groupes, et les erreurs.
Le label : le mécanisme complet
Un champ tout seul ne sait pas comment il s'appelle. Le <label> est ce qui lui donne un nom. Et la liaison se fait par une paire d'identifiants :
<label for="email">Adresse email</label>
<input id="email" type="email" name="email">
Le for="email" du label pointe vers le id="email" du champ. Même valeur des deux côtés : c'est ce fil invisible qui relie l'étiquette au champ. Et ce fil rapporte trois choses, pas une :
- Le nom dans le lecteur d'écran. Marie arrive sur le champ et entend « Adresse email, champ de saisie ». Elle sait quoi taper. Le label a fourni le nom accessible du champ, celui qui remonte dans l'arbre d'accessibilité qu'on a vu en leçon 1.
- Une cible de clic géante, gratuite. Cliquer sur le mot « Adresse email » focuse le champ, comme si on avait cliqué dans le champ lui-même. La zone cliquable s'agrandit du texte tout entier. C'est exactement la cible de 24px minimum vue en leçon 3 : ici, on l'obtient sans rien faire de plus.
- Une aide pour tout le monde. Sur mobile, viser une case à cocher minuscule au doigt est pénible ; pouvoir cliquer son label entier rend la cible confortable pour les mains tremblantes, les écrans tactiles, et les jours de fatigue.
Un seul élément, trois bénéfices, zéro JavaScript. Le label n'est pas une décoration : c'est l'infrastructure du champ.
« Le placeholder suffit, non ? »
C'est l'erreur la plus répandue, et la plus tentante : le texte grisé à l'intérieur du champ (le placeholder) ressemble à une étiquette, alors pourquoi s'embêter avec un label ?
Le placeholder ne remplace jamais un label. Il pose quatre problèmes d'un coup :
- Il disparaît à la frappe. Dès que l'utilisateur tape une lettre, le texte d'indication s'efface. S'il oublie ce qu'on lui demandait, il doit tout effacer pour le relire. C'est une charge mémoire imposée pour rien.
- Son contraste est souvent insuffisant. Le gris pâle des placeholders passe rarement les ratios de contraste (la leçon 6 va creuser ça). Beaucoup d'utilisateurs ne le lisent même pas.
- Il n'est pas lu de façon fiable. Tous les lecteurs d'écran n'annoncent pas le placeholder, et certains le lisent comme une valeur déjà saisie. On ne peut pas compter dessus pour nommer le champ.
- Il ne se clique pas. Aucune cible agrandie : on perd le bénéfice du label cliquable.
Le placeholder a un seul vrai usage : montrer un exemple de format, en plus du label. « Adresse email » dans le label, « jean@exemple.fr » dans le placeholder. L'exemple aide ; il ne nomme pas.
Grouper les choix : fieldset et legend
Un champ texte se nomme tout seul avec son label. Mais que faire d'un groupe de boutons radio, où plusieurs choix partagent une seule question ?
<fieldset>
<legend>Mode de livraison</legend>
<label><input type="radio" name="livraison" value="domicile"> À domicile</label>
<label><input type="radio" name="livraison" value="point-relais"> Point relais</label>
<label><input type="radio" name="livraison" value="magasin"> En magasin</label>
</fieldset>
Le <fieldset> dessine la frontière du groupe, et le <legend> lui donne son titre. Sans eux, le lecteur d'écran annonce « À domicile, bouton radio » tout seul, hors contexte : à domicile de quoi ? Avec le legend, il annonce « Mode de livraison, À domicile, bouton radio ». La question revient à chaque option, et l'utilisateur sait toujours de quoi il s'agit.
Règle d'or : dès que plusieurs champs forment une seule question (boutons radio d'un choix unique, cases à cocher d'une même liste, adresse découpée en plusieurs champs), enveloppe-les dans un <fieldset> avec un <legend>. Le label nomme un champ ; le couple fieldset/legend nomme un groupe.
Les erreurs : ton premier morceau d'ARIA
C'est le cas critique. Le formulaire est rempli, l'utilisateur valide, et un champ est faux. Comment le lui dire ?
L'anti-pattern classique : une bordure rouge, et rien d'autre.
La bordure rouge seule est invisible pour la moitié de tes utilisateurs. Marie, au lecteur d'écran, ne voit aucune couleur : pour elle, le champ n'a tout simplement pas d'erreur. Et un utilisateur daltonien (autour d'un homme sur douze) peut ne pas distinguer ce rouge du reste : il voit un champ, sans comprendre ce qui cloche. L'information portée par la couleur seule n'arrive jamais à tout le monde. Retiens ce piège : il revient en force à la leçon 6.
Le mécanisme correct repose sur deux choses. D'abord, un message d'erreur textuel, écrit en clair sous le champ : « Cette adresse email n'est pas valide. » Du texte, lisible par tout le monde, lecteur d'écran compris. Ensuite, il faut relier ce message au champ, pour que le lecteur d'écran l'annonce au bon moment et pas comme un texte perdu dans la page.
Et c'est ici qu'apparaît ton premier morceau d'ARIA. Jusqu'à cette leçon, le bon HTML suffisait. Mais relier un message d'erreur à un champ, le HTML pur ne sait pas le faire : il n'existe pas d'attribut natif pour ça. ARIA comble exactement ce trou, avec deux attributs :
<label for="email">Adresse email</label>
<input id="email" type="email" name="email"
aria-invalid="true"
aria-describedby="email-erreur">
<p id="email-erreur" class="erreur">Cette adresse email n'est pas valide.</p>
aria-invalid="true"dit au lecteur d'écran : « ce champ est en erreur ». Il l'annoncera comme tel (« champ non valide »).aria-describedby="email-erreur"relie le champ au paragraphe d'erreur, par le même jeu d'identifiants quefor/id: l'attribut pointe vers l'iddu message. Du coup, quand Marie arrive sur le champ, le lecteur lit le label PUIS le message d'erreur : « Adresse email, champ non valide. Cette adresse email n'est pas valide. » Elle sait quoi corriger, sans jamais voir la moindre bordure rouge.
Voilà l'idée d'ARIA en une phrase : on n'y touche que là où le HTML ne sait plus faire. La leçon 7 généralisera ce réflexe à tout ARIA. Pour l'instant, retiens le couple aria-invalid + aria-describedby : c'est le standard pour des erreurs de formulaire accessibles.
Un champ a un placeholder="Votre email" et aucun <label>. Marie tape trois lettres au lecteur d'écran, puis y revient. Qu'est-ce que son logiciel annonce sur ce champ à ce moment-là ?
Voir la réponse
« Champ de saisie », sans nom. Le placeholder a disparu dès la première lettre tapée : il ne reste donc plus aucune indication à l'écran. Et comme le placeholder n'est pas un nom accessible fiable, le champ n'a tout simplement pas de nom dans l'arbre d'accessibilité. Marie entend « champ de saisie, email-saisi » au mieux, sans savoir que c'était l'email demandé. Un vrai <label for="email"> aurait donné un nom permanent, qui ne disparaît jamais à la frappe.
Les deux liaisons par identifiant
Tout repose sur le même fil : un attribut qui pointe vers un id. Le for du label relie l'étiquette au champ ; l'aria-describedby relie le champ à son message d'erreur. Garde ces deux flèches en tête.
aria-describedby lui rattache son erreur. Même fil, deux usages.À toi : répare le formulaire cassé
Voici un formulaire d'inscription truffé des trois pièges de la leçon : un champ avec un placeholder en guise de label, des boutons radio sans fieldset/legend, et une erreur signalée en couleur seule. Répare-le pour qu'il se laisse remplir sans les yeux. Clique sur Exécuter pour voir l'aperçu, puis sur Vérifier.
Ton objectif : un <label for="..."> relié à chaque champ, un <fieldset> avec <legend> autour des radios, et l'erreur reliée au champ par aria-describedby (plus aria-invalid="true").
Voir la solution complète
<form action="/inscription" method="POST">
<label for="email">Adresse email</label>
<input type="email" id="email" name="email" placeholder="jean@exemple.fr">
<fieldset>
<legend>Mode de livraison</legend>
<label><input type="radio" name="livraison" value="domicile"> À domicile</label>
<label><input type="radio" name="livraison" value="relais"> Point relais</label>
</fieldset>
<label for="cp">Code postal</label>
<input type="text" id="cp" name="cp"
aria-invalid="true"
aria-describedby="cp-erreur">
<p id="cp-erreur">Le code postal doit contenir 5 chiffres.</p>
<button type="submit">S'inscrire</button>
</form>
Le placeholder est redevenu un simple exemple (et non le nom du champ). Les radios ont un titre de groupe. L'erreur est désormais du texte relié au champ : Marie l'entendra en arrivant sur le champ, et le daltonien la lira.
The signup form, seen without eyes
Picture your signup form. With a mouse, all is well: you click the field, you see the label right above it, you type. The gesture is obvious.
Now close your eyes. Marie, who uses a screen reader, doesn't have that label in front of her. Her software lands on the field and announces... "edit text". For what? A mystery. Name, email, password? No idea. She types blind, submits, and the form throws her back with an error. She gives up.
The form is where accessibility matters most, because that's where the user has to act, not just read. And the good news, as always, is that almost everything is solved with the right HTML. This lesson gives you the three mechanisms that turn a mute form into one you can fill in without your eyes: the label, the groups, and the errors.
The label: the full mechanism
A field on its own doesn't know its own name. The <label> is what gives it one. And the link is made through a pair of identifiers:
<label for="email">Email address</label>
<input id="email" type="email" name="email">
The label's for="email" points to the field's id="email". Same value on both sides: that's the invisible thread tying the label to the field. And that thread brings back three things, not one:
- The name in the screen reader. Marie lands on the field and hears "Email address, edit text". She knows what to type. The label provided the field's accessible name, the one that surfaces in the accessibility tree we saw in lesson 1.
- A giant click target, for free. Clicking the words "Email address" focuses the field, as if you had clicked in the field itself. The clickable zone grows to the whole text. That's exactly the 24px minimum target from lesson 3, and here you get it for nothing.
- Help for everyone. On mobile, hitting a tiny checkbox with a finger is painful; being able to click its entire label makes the target comfortable for shaky hands, touchscreens, and tired days.
One element, three benefits, zero JavaScript. The label isn't decoration: it's the field's infrastructure.
"The placeholder is enough, right?"
It's the most common mistake, and the most tempting: the greyed text inside the field (the placeholder) looks like a label, so why bother with one?
A placeholder never replaces a label. It causes four problems at once:
- It vanishes as you type. The moment the user types a letter, the hint text disappears. If they forget what was asked, they have to erase everything to read it again. That's a memory load imposed for nothing.
- Its contrast is often too low. The pale grey of placeholders rarely passes contrast ratios (lesson 6 will dig into that). Many users can't even read it.
- It isn't read reliably. Not all screen readers announce the placeholder, and some read it as an already-entered value. You can't rely on it to name the field.
- It can't be clicked. No enlarged target: you lose the clickable-label benefit.
The placeholder has one legitimate use: showing a format example, in addition to the label. "Email address" in the label, "jane@example.com" in the placeholder. The example helps; it doesn't name.
Grouping choices: fieldset and legend
A text field names itself with its label. But what about a group of radio buttons, where several choices share a single question?
<fieldset>
<legend>Delivery method</legend>
<label><input type="radio" name="delivery" value="home"> Home</label>
<label><input type="radio" name="delivery" value="pickup"> Pickup point</label>
<label><input type="radio" name="delivery" value="store"> In store</label>
</fieldset>
The <fieldset> draws the group's boundary, and the <legend> gives it a title. Without them, the screen reader announces "Home, radio button" on its own, out of context: home for what? With the legend, it announces "Delivery method, Home, radio button". The question comes back on every option, and the user always knows what it's about.
Golden rule: whenever several fields form one single question (radio buttons of one choice, checkboxes of one list, an address split into several fields), wrap them in a <fieldset> with a <legend>. The label names a field; the fieldset/legend pair names a group.
Errors: your first piece of ARIA
This is the critical case. The form is filled, the user submits, and a field is wrong. How do you tell them?
The classic anti-pattern: a red border, and nothing else.
A red border alone is invisible to half your users. Marie, on a screen reader, sees no colour: for her, the field simply has no error. And a colour-blind user (around one man in twelve) may not tell that red from the rest: they see a field, without understanding what's wrong. Information carried by colour alone never reaches everyone. Remember this trap: it comes back in force in lesson 6.
The correct mechanism rests on two things. First, a text error message, written in plain words below the field: "This email address isn't valid." Text, readable by everyone, screen reader included. Then, you must link that message to the field, so the screen reader announces it at the right moment and not as text lost in the page.
And here's where your first piece of ARIA appears. Until this lesson, good HTML was enough. But linking an error message to a field is something plain HTML can't do: there's no native attribute for it. ARIA fills exactly that gap, with two attributes:
<label for="email">Email address</label>
<input id="email" type="email" name="email"
aria-invalid="true"
aria-describedby="email-error">
<p id="email-error" class="error">This email address isn't valid.</p>
aria-invalid="true"tells the screen reader: "this field is in error". It will announce it as such ("invalid field").aria-describedby="email-error"links the field to the error paragraph, through the same identifier game asfor/id: the attribute points to the message'sid. So when Marie reaches the field, the reader reads the label THEN the error message: "Email address, invalid field. This email address isn't valid." She knows what to fix, without ever seeing a red border.
That's the idea of ARIA in one sentence: you only touch it where HTML can no longer cope. Lesson 7 will generalise this reflex to all of ARIA. For now, remember the pair aria-invalid + aria-describedby: it's the standard for accessible form errors.
A field has a placeholder="Your email" and no <label>. Marie types three letters on a screen reader, then comes back to it. What does her software announce on that field at this point?
Show the answer
"Edit text", with no name. The placeholder vanished on the first letter typed: nothing is left on screen as a hint. And since the placeholder isn't a reliable accessible name, the field simply has no name in the accessibility tree. Marie hears "edit text, ema" at best, without knowing it was the requested email. A real <label for="email"> would have given a permanent name, one that never vanishes as you type.
The two identifier links
Everything rests on the same thread: an attribute pointing to an id. The label's for links the label to the field; the aria-describedby links the field to its error message. Keep these two arrows in mind.
aria-describedby attaches its error. Same thread, two uses.Your turn: repair the broken form
Here's a signup form stuffed with the lesson's three traps: a field with a placeholder used as a label, radio buttons with no fieldset/legend, and an error signalled by colour alone. Repair it so it can be filled without eyes. Click Run to see the preview, then Check.
Your goal: a <label for="..."> linked to each field, a <fieldset> with a <legend> around the radios, and the error linked to the field via aria-describedby (plus aria-invalid="true").
Show the full solution
<form action="/signup" method="POST">
<label for="email">Email address</label>
<input type="email" id="email" name="email" placeholder="jane@example.com">
<fieldset>
<legend>Delivery method</legend>
<label><input type="radio" name="delivery" value="home"> Home</label>
<label><input type="radio" name="delivery" value="pickup"> Pickup point</label>
</fieldset>
<label for="zip">Postcode</label>
<input type="text" id="zip" name="zip"
aria-invalid="true"
aria-describedby="zip-error">
<p id="zip-error">The postcode must contain 5 digits.</p>
<button type="submit">Sign up</button>
</form>
The placeholder is back to being a mere example (and not the field's name). The radios have a group title. The error is now text linked to the field: Marie will hear it on reaching the field, and the colour-blind user will read it.
🎯 Pratique
S'entraîner (clique pour ouvrir) :
💬 Ré-explique sans regarder
Explique à un collègue comment un lecteur d'écran sait qu'un champ s'appelle « Adresse email », et comment il sait que ce champ est en erreur. Cite les attributs en jeu.
<label for="email"> relié à l'<input id="email"> par la même valeur d'identifiant ; ce label devient le nom accessible du champ. Pour l'erreur, aria-invalid="true" marque le champ comme non valide, et aria-describedby pointe vers l'id du paragraphe d'erreur, que le lecteur lit après le nom du champ. Le placeholder ne joue aucun de ces deux rôles.🧠 Rappel libre
Sans remonter : cite au moins deux raisons pour lesquelles un placeholder ne peut pas remplacer un <label>.
⚖️ Juge le code de l'IA
Tu demandes à l'IA un formulaire d'inscription accessible. Elle répond : « Voilà, accessible et joli ! » avec des champs stylés en labels flottants : chaque champ a juste un placeholder="Votre email" que le CSS fait remonter au-dessus quand on tape. Aucun <label> dans le HTML. Tu acceptes, ou tu rejettes ?
placeholder stylé en CSS n'est pas un label : le champ n'a aucun nom accessible dans l'arbre, le texte disparaît à la frappe pour le lecteur d'écran, et il ne se clique pas. C'est joli à l'œil, muet à l'oreille. Le bon correctif : garder un vrai <label for="email"> dans le HTML (le CSS peut toujours l'animer en label flottant), et réserver le placeholder à un exemple de format. Règle d'or : un label, c'est une balise <label>, pas un effet visuel.<label> à un champ avec for et id identiques ?Tu as croisé deux fois le même piège : l'information portée par la couleur seule. La bordure rouge muette, le placeholder gris pâle. Leçon 6 : le contraste, les ratios WCAG, et le daltonisme qu'on oublie toujours.
Leçon 6 : Contraste et couleur →