Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 523 minDébutant

Formulaire HTML : input, label et validation

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • créer un formulaire avec action et method ;
  • choisir le bon type de champ et associer chaque champ à son label ;
  • valider les saisies avec required, pattern, min et max ;
  • afficher des messages d'erreur accessibles ;
  • expliquer pourquoi la validation côté serveur reste indispensable.

4 exercices corrigés

Un formulaire HTML est un ensemble de champs (<input>, <select>, <textarea>) placés dans une balise <form>, dont les valeurs sont envoyées à un serveur quand l'utilisateur clique sur le bouton d'envoi. Ce chapitre montre comment construire un formulaire complet et accessible, comment le navigateur vérifie les saisies avec required, type et pattern, et pourquoi cette validation ne remplace jamais celle du serveur.

La balise form : action et method

html
<form action="inscription.php" method="post">
  <label for="nom">Nom</label>
  <input type="text" id="nom" name="nom">
  <button type="submit">S'inscrire</button>
</form>
AttributRôle
actionl'adresse du programme qui reçoit les données (un script PHP, une route d'API). Absent, le formulaire est envoyé à la page elle-même.
methodla façon d'envoyer : get (par défaut) ou post
enctype="multipart/form-data"obligatoire, avec method="post", dès que le formulaire envoie un fichier

Seuls les champs qui ont un attribut name sont envoyés : c'est le nom sous lequel le serveur retrouvera la valeur ($_POST['nom'] en PHP). Un champ sans name s'affiche, se remplit, mais n'arrive jamais au serveur. C'est l'oubli le plus fréquent.

GET ou POST ? Avec get, les données sont ajoutées à l'adresse :

text
https://www.mediatheque-tilleuls.fr/recherche.php?q=harry+potter&type=livre

Avec post, elles voyagent dans le corps de la requête HTTP, invisibles dans l'adresse.

GETPOST
Où sont les donnéesdans l'URL, après ?dans le corps de la requête
Visibles dans l'historique, les favoris, les journaux du serveurouinon
Usageune recherche, un filtre : on peut partager ou mettre en favori le résultatune action qui modifie quelque chose : inscription, connexion, commande, envoi de fichier

Les champs input et leurs types

L'élément <input> change de nature selon son attribut type. Le bon type apporte trois choses gratuites : un clavier adapté sur téléphone, une vérification du format par le navigateur, et parfois une interface dédiée (calendrier, palette).

TypePourCe que fait le navigateur
textun texte court (par défaut)rien de particulier
emailune adresse e-mailclavier avec @, vérifie la forme x@y
telun numéro de téléphoneclavier numérique ; aucune vérification de format (les formats varient selon les pays)
urlune adresse webvérifie la présence d'un protocole (https://)
passwordun mot de passemasque les caractères ; le gestionnaire de mots de passe le repère
numberune quantitéflèches + et −, min, max, step
date, time, datetime-localune date, une heuresélecteur de date ; la valeur envoyée est au format ISO 2026-06-03
searchun champ de recherchebouton d'effacement, touche « Rechercher » sur mobile
checkboxune case à cocher (oui/non, choix multiples)
radioun choix unique parmi plusieursles boutons de même name s'excluent
fileun envoi de fichieraccept="image/*,.pdf" filtre le sélecteur
range, colorun curseur, une couleur
hiddenune valeur envoyée mais non affichéevisible et modifiable par qui ouvre l'inspecteur

label : une étiquette pour chaque champ

Chaque champ a une étiquette <label>, reliée au champ par son attribut for, qui reprend l'id du champ :

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

Ce lien n'est pas décoratif :

  • un clic sur l'étiquette place le curseur dans le champ, ou coche la case : une cible bien plus grande, précieuse sur téléphone et pour les personnes qui ont du mal à viser ;
  • un lecteur d'écran annonce « Adresse e-mail, zone d'édition » en arrivant sur le champ ; sans <label>, il dit seulement « zone d'édition », et la personne ne sait pas quoi taper.

On peut aussi mettre le champ dans le <label>, sans for ni id : <label>Nom <input name="nom"></label>. La première forme est plus souple pour la mise en page.

textarea, select, boutons radio et cases à cocher

html
<!-- Zone de texte sur plusieurs lignes -->
<label for="message">Votre message</label>
<textarea id="message" name="message" rows="5" maxlength="1000"></textarea>

<!-- Liste déroulante -->
<label for="atelier">Atelier</label>
<select id="atelier" name="atelier">
  <option value="">-- Choisir un atelier --</option>
  <optgroup label="Numérique">
    <option value="initiation">Initiation à l'ordinateur</option>
    <option value="demarches">Démarches en ligne</option>
  </optgroup>
  <optgroup label="Lecture">
    <option value="club-ados">Club lecture ados</option>
  </optgroup>
</select>

La valeur envoyée est celle de l'attribut value (initiation), pas le texte affiché. Ajoute multiple pour autoriser plusieurs choix (et un name qui se termine par [] si le serveur est en PHP).

Des boutons radio ou des cases à cocher qui répondent à une même question se regroupent dans un <fieldset>, avec la question dans une <legend> :

html
<fieldset>
  <legend>Tranche d'âge</legend>
  <input type="radio" id="age-enfant" name="age" value="enfant">
  <label for="age-enfant">Moins de 12 ans</label>
  <input type="radio" id="age-ado" name="age" value="ado">
  <label for="age-ado">12 à 17 ans</label>
  <input type="radio" id="age-adulte" name="age" value="adulte" checked>
  <label for="age-adulte">18 ans et plus</label>
</fieldset>

<fieldset>
  <legend>Comment avez-vous connu l'atelier ?</legend>
  <input type="checkbox" id="src-site" name="source[]" value="site">
  <label for="src-site">Le site</label>
  <input type="checkbox" id="src-affiche" name="source[]" value="affiche">
  <label for="src-affiche">Une affiche</label>
</fieldset>
  • Les boutons radio d'un même groupe partagent le même name : c'est ce qui les rend exclusifs. Chacun a sa propre value.
  • checked coche par défaut. Une case non cochée n'envoie rien : côté serveur, l'absence de valeur signifie « non ».
  • Le lecteur d'écran annonce la légende avec chaque option : « Tranche d'âge, groupe ; 12 à 17 ans, bouton radio, 2 sur 3 ».

Enfin, les boutons :

BoutonEffet
<button type="submit">S'inscrire</button>envoie le formulaire (c'est le type par défaut d'un <button> dans un <form>)
<button type="button">Ajouter un participant</button>ne fait rien seul : un script lui donne un rôle
<button type="reset">vide tout le formulaire ; presque toujours une mauvaise idée, qu'on clique par erreur

autocomplete : aider le navigateur à remplir

L'attribut autocomplete dit au navigateur quelle information attend le champ, pour qu'il propose les données que l'utilisateur a enregistrées :

html
<input type="text" id="prenom" name="prenom" autocomplete="given-name">
<input type="text" id="nom" name="nom" autocomplete="family-name">
<input type="email" id="email" name="email" autocomplete="email">
<input type="tel" id="tel" name="tel" autocomplete="tel">
<input type="text" id="cp" name="cp" autocomplete="postal-code" inputmode="numeric">
<input type="password" id="mdp" name="mdp" autocomplete="new-password">

Pour une personne qui a du mal à taper (handicap moteur, troubles cognitifs) ou qui remplit sur téléphone, c'est un gain énorme. C'est d'ailleurs un critère d'accessibilité : WCAG 1.3.5 et RGAA 11.13 demandent d'indiquer la finalité des champs qui concernent l'utilisateur. Pour un mot de passe, current-password sert à la connexion, new-password à la création de compte (le gestionnaire propose alors un mot de passe solide).

La validation native : required, pattern, min et max

Le navigateur sait vérifier les saisies avant l'envoi, sans une ligne de JavaScript. Si un champ est invalide, il bloque l'envoi, place le curseur dans le premier champ en erreur et affiche une bulle d'explication.

AttributVérifie que…Exemple
requiredle champ n'est pas vide (la case est cochée, une option est choisie)<input name="nom" required>
type="email", type="url"le format est plausiblea@b passe, ab.fr non
minlength, maxlengthle nombre de caractères est dans les bornes (maxlength empêche même de taper au-delà)minlength="2" maxlength="50"
min, max, stepla valeur d'un nombre ou d'une date est dans les bornestype="number" min="1" max="4"
patternla valeur correspond en entier à une expression régulièrepattern="[0-9]{5}" pour un code postal
html
<label for="cp">Code postal (5 chiffres)</label>
<input type="text" id="cp" name="cp" inputmode="numeric"
       pattern="[0-9]{5}" required autocomplete="postal-code">

<label for="date">Date souhaitée</label>
<input type="date" id="date" name="date" min="2026-06-01" max="2026-06-30" required>

Le pattern s'applique à toute la valeur : [0-9]{5} refuse 750001 (six chiffres) sans qu'il soit besoin d'écrire ^ et $. Il n'est vérifié que si le champ n'est pas vide : ajoute required si la saisie est obligatoire. Indique toujours le format attendu dans l'étiquette (« 5 chiffres ») : le message d'erreur du navigateur se contente souvent de « Veuillez respecter le format requis ».

En CSS, les pseudo-classes :valid et :invalid permettent de colorer les champs. Mais :invalid s'applique dès l'affichage : un formulaire vierge s'afficherait tout en rouge. Utilise plutôt :user-invalid, reconnue par tous les navigateurs actuels, qui n'agit qu'une fois que l'utilisateur a modifié le champ ou tenté d'envoyer :

css
input:user-invalid,
select:user-invalid {
  border: 2px solid #b91c1c;
}

L'attribut novalidate sur <form> désactive la validation du navigateur. On s'en sert quand un script JavaScript prend le relais pour afficher ses propres messages, plus clairs et mieux placés.

Des messages d'erreur accessibles

Les bulles d'erreur natives du navigateur disparaissent au bout de quelques secondes et se lisent mal avec certaines aides techniques. Pour un formulaire important, on affiche des messages dans la page, sous le champ concerné. Trois règles pour qu'ils soient compris de tous :

  1. Le message est un texte, pas seulement une bordure rouge : une personne daltonienne ne voit pas la différence, un lecteur d'écran ne lit pas les couleurs.
  2. Le message est relié au champ par aria-describedby, et le champ est marqué aria-invalid="true" : le lecteur d'écran annonce « Adresse e-mail, invalide, saisie obligatoire. Exemple : nom@domaine.fr ».
  3. Le message dit comment corriger, pas seulement qu'il y a une erreur : « Saisissez une adresse de la forme nom@domaine.fr » plutôt que « Champ invalide ».
html
<label for="email">Adresse e-mail (obligatoire)</label>
<input type="email" id="email" name="email" required autocomplete="email"
       aria-invalid="true" aria-describedby="email-erreur">
<p id="email-erreur" class="erreur">
  Adresse e-mail invalide : saisissez une adresse de la forme nom@domaine.fr.
</p>

Sur un long formulaire, ajoute en haut, après l'envoi, un récapitulatif des erreurs avec un lien vers chaque champ, et place le focus dessus. Signale aussi les champs obligatoires avant la saisie, dans l'étiquette (« (obligatoire) ») ou avec un astérisque expliqué en tête de formulaire.

Les limites de la validation côté client

Tout ce qui précède se passe dans le navigateur de l'utilisateur, c'est-à-dire sur une machine que tu ne contrôles pas. N'importe qui peut :

  • supprimer l'attribut required ou pattern dans l'inspecteur, puis envoyer ;
  • modifier la valeur d'un champ hidden ou d'une option de <select> ;
  • envoyer directement une requête au serveur, sans passer par ton formulaire :
bash
curl -X POST https://www.mediatheque-tilleuls.fr/inscription.php \
     -d "nom=" -d "email=pas-une-adresse" -d "atelier=atelier-qui-n-existe-pas"

La validation côté client sert le confort de l'utilisateur : il voit ses erreurs tout de suite, sans aller-retour. La sécurité et la cohérence des données reposent uniquement sur la validation côté serveur.

Pense enfin au RGPD : un formulaire ne demande que les données nécessaires (pas de date de naissance pour s'inscrire à un atelier), et il indique sous le bouton d'envoi qui traite les données, pourquoi, combien de temps, et comment exercer ses droits (chapitre 14).

Un formulaire complet : l'inscription à un atelier

html
<form action="inscription.php" method="post">
  <p>Tous les champs sont obligatoires, sauf mention contraire.</p>

  <fieldset>
    <legend>Vos coordonnées</legend>

    <label for="prenom">Prénom</label>
    <input type="text" id="prenom" name="prenom" required maxlength="50" autocomplete="given-name">

    <label for="nom">Nom</label>
    <input type="text" id="nom" name="nom" required maxlength="50" autocomplete="family-name">

    <label for="email">Adresse e-mail</label>
    <input type="email" id="email" name="email" required autocomplete="email"
           aria-describedby="email-aide">
    <p id="email-aide" class="aide">La confirmation d'inscription sera envoyée à cette adresse.</p>

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

  <fieldset>
    <legend>Votre atelier</legend>

    <label for="atelier">Atelier</label>
    <select id="atelier" name="atelier" required>
      <option value="">-- Choisir un atelier --</option>
      <option value="initiation">Initiation à l'ordinateur – 3 juin</option>
      <option value="demarches">Démarches en ligne – 10 juin</option>
    </select>

    <label for="places">Nombre de places (1 à 4)</label>
    <input type="number" id="places" name="places" min="1" max="4" value="1" required>
  </fieldset>

  <input type="checkbox" id="rappel" name="rappel" value="oui">
  <label for="rappel">Recevoir un rappel par e-mail la veille (facultatif)</label>

  <button type="submit">Envoyer mon inscription</button>

  <p class="rgpd">Vos données servent uniquement à gérer votre inscription et sont supprimées
  un mois après l'atelier. <a href="donnees-personnelles.html">Vos données et vos droits</a>.</p>
</form>
css
form {
  display: grid;
  gap: 0.5rem;
  max-width: 32rem;
}

fieldset {
  display: grid;
  gap: 0.5rem;
  border: 1px solid #94a3b8;
  border-radius: 8px;
  padding: 1rem;
}

label {
  font-weight: 600;
}

input,
select,
textarea {
  font: inherit;               /* les champs n'héritent pas de la police par défaut */
  padding: 0.5rem;
  border: 1px solid #64748b;
  border-radius: 4px;
}

input:focus-visible,
select:focus-visible {
  outline: 3px solid #0f766e;  /* un focus bien visible pour la navigation au clavier */
  outline-offset: 2px;
}

input:user-invalid,
select:user-invalid {
  border: 2px solid #b91c1c;
}

.aide {
  font-size: 0.9rem;
  color: #475569;
}

Exercices

Exercice 1 sur 4 Le bon type de champ Réussi À revoir

Indique le type (et si besoin inputmode ou autocomplete) le plus adapté pour chaque information.

  1. L'adresse e-mail d'un adhérent.
  2. Son numéro de téléphone portable.
  3. Le numéro de sa carte d'adhérent : 8 chiffres, qui peut commencer par 0.
  4. Le nombre de documents à réserver, entre 1 et 10.
  5. Le mot de passe choisi à la création du compte.
  6. Un justificatif de domicile en PDF ou en image.
  7. La date de retour souhaitée.
Voir la réponseMasquer la réponse de l'exercice 1
  1. type="email" autocomplete="email".
  2. type="tel" autocomplete="tel" : clavier numérique, pas de contrôle de format imposé.
  3. type="text" inputmode="numeric" pattern="[0-9]{8}" : un numéro n'est pas une quantité, et type="number" supprimerait le zéro initial.
  4. type="number" min="1" max="10".
  5. type="password" autocomplete="new-password", avec minlength selon la politique de mots de passe.
  6. type="file" accept=".pdf,image/*", dans un formulaire en method="post" avec enctype="multipart/form-data". Le serveur revérifie le type réel du fichier : accept ne filtre que la fenêtre de sélection.
  7. type="date", avec min et max pour limiter aux dates possibles.

Tu avais trouvé ?

Exercice 2 sur 4 Pourquoi rien n'arrive au serveur ? Réussi À revoir

Ce formulaire s'affiche bien, mais le script contact.php ne reçoit ni le nom ni le message. Trouve les problèmes, puis réécris le formulaire correctement et de façon accessible.

html
<form action="contact.php">
  Nom : <input type="text" id="nom">
  <input type="text" placeholder="Votre message" name="message">
  <div onclick="envoyer()">Envoyer</div>
</form>
Voir la réponseMasquer la réponse de l'exercice 2
  • Le champ du nom n'a pas d'attribut name : il n'est jamais envoyé.
  • Le formulaire n'a pas de method : il part en GET, le message se retrouve dans l'URL. Pour un message de contact, method="post".
  • Le « bouton » est un <div> : il ne reçoit pas le focus au clavier, n'envoie pas le formulaire avec Entrée et n'est pas annoncé comme un bouton. Le message n'est probablement pas envoyé non plus si le script envoyer() ne soumet pas le formulaire.
  • Aucune étiquette <label> : « Nom : » est un texte libre, et le message n'a qu'un placeholder.
  • Un message tient sur plusieurs lignes : <textarea>.
html
<form action="contact.php" method="post">
  <label for="nom">Nom</label>
  <input type="text" id="nom" name="nom" required autocomplete="name">

  <label for="message">Votre message</label>
  <textarea id="message" name="message" rows="5" required maxlength="2000"></textarea>

  <button type="submit">Envoyer le message</button>
</form>

Tu avais trouvé ?

Exercice 3 sur 4 Écrire des pattern Réussi À revoir

Écris l'attribut pattern pour chaque format, en te rappelant qu'il doit couvrir toute la valeur.

  1. Un code postal français : exactement 5 chiffres.
  2. Un identifiant d'adhérent : les lettres TIL, un tiret, puis 4 chiffres (exemple : TIL-0427).
  3. Un numéro de téléphone français à 10 chiffres commençant par 0, sans espaces.
  4. Un prénom de 2 à 30 caractères, lettres (accentuées comprises), espaces et tirets.
Voir la réponseMasquer la réponse de l'exercice 3
  1. pattern="[0-9]{5}".
  2. pattern="TIL-[0-9]{4}".
  3. pattern="0[0-9]{9}" : un 0, puis 9 chiffres, soit 10 en tout.
  4. pattern="[\p{L} \-]{2,30}" : \p{L} désigne n'importe quelle lettre Unicode, accentuées comprises ; le tiret est échappé (\-), parce que les navigateurs analysent pattern avec le drapeau v des expressions régulières, qui l'exige dans une classe de caractères.

Vérifie-les dans la console du navigateur : /^(?:[0-9]{5})$/v.test("01000") renvoie true, /^(?:0[0-9]{9})$/v.test("0612345678") renvoie true, /^(?:[\p{L} \-]{2,30})$/v.test("Anne-Sophie") renvoie true. Le serveur doit refaire les mêmes vérifications.

Tu avais trouvé ?

Exercice 4 sur 4 Côté client ou côté serveur ? Réussi À revoir

Pour chaque contrôle, dis s'il peut être fait côté client, s'il doit être fait côté serveur, ou les deux.

  1. Le champ e-mail n'est pas vide.
  2. L'adresse e-mail n'est pas déjà inscrite à cet atelier.
  3. Il reste des places dans l'atelier choisi.
  4. Le nombre de places demandées est entre 1 et 4.
  5. Le fichier envoyé est bien un PDF de moins de 2 Mo.
Voir la réponseMasquer la réponse de l'exercice 4
  1. Les deux : required pour le confort, revérifié par le serveur.
  2. Serveur uniquement : il faut interroger la base de données.
  3. Serveur uniquement, au moment de l'enregistrement : entre l'affichage de la page et l'envoi, d'autres personnes ont pu prendre les dernières places (voir la gestion de la concurrence en SQL).
  4. Les deux : min="1" max="4" côté client, et le serveur refuse 0, 5 ou -3 envoyés avec curl.
  5. Les deux : accept=".pdf" aide à choisir le bon fichier, mais seul le serveur peut vérifier le type réel (le contenu, pas l'extension) et la taille.

Tu avais trouvé ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Les bases du CSS : sélecteurs, couleurs, unités et policesRevoir le sommaire du cours