Développement Web

Créer des formulaires HTML accessibles

13 min de lecture

Un site sans formulaire est un site qui ne sait pas écouter. Inscription, contact, recherche, adhésion : le formulaire est le point où votre visiteur cesse d’être spectateur pour devenir acteur. Et c’est précisément là que se jouent l’accessibilité et la qualité perçue d’un site. Un champ mal étiqueté, et une personne utilisant un lecteur d’écran ne saura jamais quoi y taper. Dans cette leçon, vous allez construire le formulaire d’adhésion de la Médiathèque Horizon — nom, e-mail, type d’abonnement, message — en respectant les règles d’accessibilité qui font la différence entre un formulaire amateur et un formulaire professionnel.

📍 À lire d’abord : Apprendre HTML & CSS de zéro — le parcours complet. Cette leçon suppose que vous maîtrisez déjà la structure sémantique d’une page.

🎯 Ce que vous allez apprendre

  • Construire un formulaire HTML complet avec <form>, champs et bouton d’envoi.
  • Associer correctement chaque champ à son étiquette avec <label> — la règle d’accessibilité numéro un.
  • Choisir le bon type de champ (email, tel, date…) pour aider l’utilisateur et déclencher le bon clavier sur mobile.
  • Regrouper des choix avec <fieldset> et <legend>, et valider les saisies sans une ligne de JavaScript.
  • Ajouter des aides et messages d’erreur lisibles par les technologies d’assistance grâce à aria-describedby.

🛠️ Ce que vous allez construire

Un formulaire d’adhésion fonctionnel : champ nom, champ e-mail vérifié par le navigateur, numéro de téléphone, choix de la formule d’abonnement par boutons radio, case à cocher de consentement, zone de message libre, et bouton « Envoyer ma demande ». À la fin, le navigateur empêchera l’envoi si un champ obligatoire est vide ou si l’e-mail est mal formé — entièrement en HTML natif. C’est la deuxième brique concrète de notre médiathèque.

Prérequis

  • Un éditeur de code et un navigateur récent.
  • Savoir écrire une page HTML de base. Si <header> et <main> ne vous évoquent rien, lisez d’abord la leçon sur la structure sémantique.
  • ⏱️ Temps estimé : environ 45 minutes.

Étape 1 — Le conteneur : la balise <form>

Tout formulaire vit à l’intérieur d’une balise <form>. Elle regroupe les champs et définit ce qui se passe à l’envoi. Deux attributs comptent : action, l’adresse vers laquelle les données sont envoyées, et method, le verbe HTTP utilisé — presque toujours post pour un envoi de données (les données ne se retrouvent pas dans l’URL, contrairement à get).

<form action="/adhesion/traitement" method="post">
  <!-- les champs viendront ici -->
</form>

Pour cet exercice pédagogique, l’action pointe vers une adresse fictive : nous nous concentrons sur le HTML côté client. Dans un vrai projet, cette adresse serait gérée par un programme côté serveur (PHP, Node.js…) qui recevrait et traiterait les données. Rechargez : la page est encore vide à l’intérieur du formulaire, c’est normal. Remplissons-la.

Étape 2 — La règle d’or : un <label> pour chaque champ

Voici le concept le plus important de toute la leçon. Chaque champ de saisie doit être accompagné d’une étiquette (<label>) qui lui est explicitement associée. Cette association se fait en deux temps : le champ reçoit un id unique, et le <label> reçoit un attribut for qui reprend exactement cet id.

<label for="nom">Nom complet</label>
<input type="text" id="nom" name="nom">

Pourquoi est-ce vital ? Trois bénéfices d’un coup. Un lecteur d’écran annoncera « Nom complet, champ de saisie » au lieu d’un « champ de saisie » anonyme et inutilisable. Cliquer sur le texte de l’étiquette place automatiquement le curseur dans le champ — une zone cliquable plus grande, précieuse sur mobile. Et l’attribut name (à ne pas confondre avec id) est la clé sous laquelle la donnée sera transmise au serveur. Retenez la distinction : id relie le label au champ dans la page ; name nomme la donnée pour l’envoi.

⚠️ Le placeholder n’est pas une étiquette. Le texte grisé d’un placeholder disparaît dès qu’on tape, n’est pas fiable pour les lecteurs d’écran et offre un contraste insuffisant. Il complète un label, il ne le remplace jamais.

Étape 3 — Choisir le bon type de champ

L’attribut type de l’<input> ne change pas seulement l’apparence : il active des comportements natifs et, sur mobile, affiche le clavier adapté. Un type="email" fait apparaître la touche « @ » ; un type="tel" affiche le pavé numérique. Voici les types les plus courants, mis en pratique dans notre formulaire :

<label for="email">Adresse e-mail</label>
<input type="email" id="email" name="email" autocomplete="email">

<label for="tel">Téléphone</label>
<input type="tel" id="tel" name="tel" autocomplete="tel">

<label for="naissance">Date de naissance</label>
<input type="date" id="naissance" name="naissance">

Le type="email" demande au navigateur de vérifier, à l’envoi, que la saisie ressemble à une adresse (présence d’un « @ », d’un domaine). Le type="date" affiche un sélecteur de calendrier natif. Quant à l’attribut autocomplete, il indique au navigateur la nature du champ pour proposer un remplissage automatique fiable — un vrai gain de confort, et une recommandation d’accessibilité (critère WCAG « Identifier la finalité des champs »). Au-delà de ceux-ci, retenez aussi number, url, password et search.

Point d’étape — Vous avez trois champs étiquetés. Sur un téléphone, vérifiez que le champ e-mail ouvre un clavier avec « @ » et le champ téléphone un pavé numérique. C’est la preuve que les types font leur travail.

Étape 4 — Regrouper des choix avec fieldset et legend

Pour l’adhésion, l’utilisateur choisit une formule parmi plusieurs : « Standard », « Étudiant », « Famille ». Quand plusieurs boutons radio forment un seul choix, il faut les regrouper sémantiquement avec <fieldset>, et donner au groupe un titre avec <legend>. Sans cela, un lecteur d’écran annonce trois options isolées sans dire à quelle question elles répondent.

Point technique crucial : tous les boutons radio d’un même groupe partagent le même attribut name. C’est ce qui les rend mutuellement exclusifs — cocher l’un décoche les autres. Chacun a en revanche un id distinct pour son label.

<fieldset>
  <legend>Formule d'abonnement</legend>

  <input type="radio" id="f-standard" name="formule" value="standard" checked>
  <label for="f-standard">Standard</label>

  <input type="radio" id="f-etudiant" name="formule" value="etudiant">
  <label for="f-etudiant">Étudiant</label>

  <input type="radio" id="f-famille" name="formule" value="famille">
  <label for="f-famille">Famille</label>
</fieldset>

L’attribut checked présélectionne la formule Standard — une bonne pratique : ne jamais laisser un groupe radio entièrement vide par défaut. L’attribut value définit la donnée envoyée au serveur quand l’option est choisie. À l’écran, vous obtenez trois ronds cliquables avec leurs libellés, encadrés et titrés « Formule d’abonnement ». Pour une case à cocher unique de consentement, on utilise type="checkbox" hors d’un fieldset, avec son propre label.

Étape 5 — Listes déroulantes et zone de texte

Quand les options sont nombreuses, une liste déroulante <select> économise l’espace. Et pour un message libre, la <textarea> offre plusieurs lignes. Ajoutons à la médiathèque le choix de l’antenne et un champ message :

<label for="antenne">Antenne souhaitée</label>
<select id="antenne" name="antenne">
  <option value="">— Choisir —</option>
  <option value="centre">Centre-ville</option>
  <option value="nord">Quartier Nord</option>
</select>

<label for="message">Votre message (facultatif)</label>
<textarea id="message" name="message" rows="4"></textarea>

La première <option> au value vide sert d’invite neutre. L’attribut rows fixe la hauteur initiale de la zone de texte. Notez que la <textarea> n’est pas auto-fermante : son contenu par défaut se place entre la balise ouvrante et fermante (ici, rien). Chaque champ a, là encore, son <label> associé — la règle ne souffre aucune exception.

Étape 6 — La validation native, sans JavaScript

Le navigateur sait empêcher l’envoi d’un formulaire incomplet, gratuitement. L’attribut booléen required rend un champ obligatoire ; minlength et maxlength bornent la longueur ; pattern impose un format via une expression régulière. Combinés au type, ils couvrent l’essentiel des besoins.

<label for="nom">Nom complet</label>
<input type="text" id="nom" name="nom" required minlength="2"
       autocomplete="name">

<label for="email">Adresse e-mail</label>
<input type="email" id="email" name="email" required
       aria-describedby="aide-email">
<small id="aide-email">Nous l'utiliserons pour confirmer votre adhésion.</small>

Ici, required sur le nom et l’e-mail bloque l’envoi tant qu’ils sont vides, avec une bulle d’erreur native dans la langue du navigateur. minlength="2" refuse un nom d’un seul caractère. Et surtout, observez l’attribut aria-describedby="aide-email" : il relie le champ au texte d’aide dont l’id est aide-email. Résultat, un lecteur d’écran lit « Adresse e-mail, obligatoire » puis « Nous l’utiliserons pour confirmer votre adhésion ». L’aide visuelle devient une aide sonore. C’est le même mécanisme qu’on emploie pour rattacher un message d’erreur à son champ.

Point d’étape — Tentez d’envoyer le formulaire avec le champ nom vide : le navigateur doit afficher une bulle « Veuillez remplir ce champ » et placer le focus sur le champ fautif. Si rien ne se passe, vérifiez que le bouton d’envoi est bien de type="submit".

Étape 7 — Le bouton d’envoi et la vérification finale

Un formulaire se termine par un bouton qui déclenche l’envoi. Utilisez l’élément <button> avec type="submit" — plus souple qu’un <input type="submit"> car il peut contenir du texte riche ou une icône.

<button type="submit">Envoyer ma demande</button>

Assemblez maintenant tous les morceaux dans une seule balise <form>, à l’intérieur du <main> de votre page d’adhésion. Rechargez, puis testez le parcours complet : laissez un champ obligatoire vide et constatez le blocage ; saisissez un e-mail sans « @ » et observez le refus ; enfin, remplissez tout correctement. Quand le navigateur accepte l’envoi (il tentera de joindre l’adresse fictive de l’action et affichera une erreur de page — c’est attendu ici), c’est que votre validation côté client fonctionne de bout en bout.

Point d’étape — Le formulaire refuse les saisies invalides et n’accepte l’envoi que lorsque nom, e-mail et consentement sont correctement remplis. La brique « adhésion » de la médiathèque est terminée.

🐞 Pièges fréquents

Symptôme / erreur Cause probable Correctif
Cliquer le label ne focalise pas le champ for du label ≠ id du champ Faire correspondre exactement for et id
Les boutons radio se cochent tous ensemble name différent sur chaque radio Donner le même name à tout le groupe
Lecteur d’écran qui ne lit pas le titre du groupe fieldset/legend absents Encadrer les radios dans un fieldset avec legend
Le champ obligatoire ne bloque pas l’envoi Bouton qui n’est pas type="submit" Mettre type="submit" sur le bouton
Donnée absente côté serveur Attribut name manquant sur le champ Ajouter un name à chaque champ à transmettre

✅ Récapitulatif

Vous avez construit un formulaire d’adhésion complet et accessible. Vous savez désormais que chaque champ exige un <label> associé par for/id, que le type de l’input améliore l’expérience et le clavier mobile, que les choix se regroupent dans un <fieldset> titré par une <legend>, et que le navigateur valide les saisies via required, minlength et pattern sans une ligne de JavaScript. Enfin, aria-describedby rattache aides et erreurs aux champs pour les rendre audibles. Un formulaire bien balisé est utilisable par tout le monde, au clavier comme à la souris, à l’œil comme à l’oreille.

🧾 Aide-mémoire

Élément / attribut Rôle
<form action method> Conteneur ; destination et verbe HTTP
<label for> Étiquette associée à un champ par son id
name Clé de la donnée envoyée au serveur
type="email/tel/date…" Comportement natif + clavier mobile adapté
<fieldset> / <legend> Regrouper et titrer un ensemble de choix
required, minlength, pattern Validation native côté navigateur
aria-describedby Rattacher une aide ou une erreur à un champ
<button type="submit"> Déclenche l’envoi du formulaire

💪 À vous de jouer

Ajoutez un champ « Nombre de personnes du foyer » accepté uniquement entre 1 et 8, et rendez le consentement (case à cocher) obligatoire avant tout envoi. Indice : pour le nombre, le bon type possède des attributs min et max.

Voir une solution
<label for="foyer">Nombre de personnes du foyer</label>
<input type="number" id="foyer" name="foyer" min="1" max="8">

<input type="checkbox" id="consent" name="consent" required>
<label for="consent">J'accepte le règlement intérieur</label>

Le type="number" avec min/max refuse les valeurs hors bornes ; required sur la case force le consentement.

Tutoriels frères

Pour aller plus loin

FAQ

Quelle différence entre id et name ?
L’id identifie l’élément dans la page (pour le relier à un label ou au CSS) ; le name nomme la donnée transmise au serveur. Un champ a souvent les deux, parfois identiques, mais leurs rôles sont distincts.

La validation HTML suffit-elle à sécuriser un formulaire ?
Non. La validation côté navigateur améliore l’expérience mais peut être contournée. Toute donnée doit toujours être revérifiée côté serveur. La validation HTML est une commodité, pas une barrière de sécurité.

Faut-il un <label> même pour un champ de recherche évident ?
Oui. Même évident à l’œil, un champ sans label reste muet pour un lecteur d’écran. Si le design impose de masquer visuellement l’étiquette, on la garde dans le code et on la cache en CSS (technique « visually-hidden »), sans jamais la supprimer.

Le type="email" garantit-il une adresse valide ?
Il vérifie seulement le format (présence de « @ » et d’un domaine), pas l’existence réelle de l’adresse. Pour confirmer qu’une boîte existe, seul un e-mail de confirmation le prouve.

Partager