Les formulaires HTML
Maîtrisez la création de formulaires interactifs et accessibles : inputs, validation, UX et sécurité.
- Inputs et champs
- Validation HTML5
- Accessibilité
- UX/UI design
Syntaxe de base des formulaires
Les formulaires HTML permettent de collecter et envoyer des données utilisateur vers un serveur.
Structure de base d’un formulaire
Éléments essentiels
html
<form> ← Conteneur principal
<label> ← Étiquette
<input> ← Champ de saisie
<button> ← Bouton d'action
</form>
Exemple minimal
html
<form action="/submit" method="POST">
<label for="name">Nom :</label>
<input type="text" id="name" name="name">
<button type="submit">Envoyer</button>
</form>
Rendu interactif
Éléments de formulaire
Découvrez tous les types d’inputs et éléments disponibles pour créer des formulaires riches.
-
Inputs texte
html
<!-- Texte simple --> <input type="text"> <!-- Email --> <input type="email"> <!-- Mot de passe --> <input type="password"> <!-- Zone de texte --> <textarea rows="3"></textarea> <!-- Recherche --> <input type="search">- Validation automatique pour email
- Password masque la saisie
- Search avec bouton d’effacement
-
Inputs numériques
html
<!-- Nombre --> <input type="number" min="0" max="100"> <!-- Curseur --> <input type="range" min="0" max="100"> <!-- Téléphone --> <input type="tel"> <!-- URL --> <input type="url">- Number avec boutons +/-
- Range avec curseur visuel
- Tel optimisé mobile
-
Date et temps
html
<!-- Date --> <input type="date"> <!-- Temps --> <input type="time"> <!-- Date et temps --> <input type="datetime-local"> <!-- Mois --> <input type="month"> <!-- Semaine --> <input type="week">- Sélecteur natif du navigateur
- Format standardisé automatique
- Validation intégrée
-
Sélections
html
<!-- Case à cocher --> <input type="checkbox" id="accept"> <label for="accept">J'accepte</label> <!-- Bouton radio --> <input type="radio" name="gender" value="M"> <input type="radio" name="gender" value="F"> <!-- Liste déroulante --> <select name="country"> <option value="fr">France</option> </select>- Checkbox : sélection multiple
- Radio : choix unique par groupe
- Select : liste déroulante
-
Fichiers et médias
html
<!-- Upload fichier --> <input type="file"> <!-- Images seulement --> <input type="file" accept="image/*"> <!-- Fichiers multiples --> <input type="file" multiple> <!-- Sélecteur couleur --> <input type="color">- Accept filtre les types
- Multiple pour plusieurs fichiers
- Color avec sélecteur visuel
-
Boutons et actions
html
<!-- Soumission --> <button type="submit">Envoyer</button> <!-- Remise à zéro --> <button type="reset">Effacer</button> <!-- Bouton normal --> <button type="button">Action</button> <!-- Input bouton --> <input type="submit" value="Valider">- Submit envoie le formulaire
- Reset vide tous les champs
- Button pour actions JavaScript
Validation HTML5
Utilisez les attributs de validation natifs pour améliorer l’expérience utilisateur.
Attributs de validation
-
required
Rend un champ obligatoire
- <input type="email" required>
-
pattern
Expression régulière pour valider le format
- <input pattern="[0-9]{5}" title="Code postal">
-
min/max & minlength/maxlength
Contraintes de valeur et de longueur
- <input type="number" min="18" max="65">
- <input minlength="8" maxlength="20">
Messages personnalisés
-
title
Message d’aide pour l’utilisateur
- title="Entrez votre code postal (5 chiffres)"
-
JavaScript personnalisé
Validation avec setCustomValidity()
- input.setCustomValidity("Message d’erreur");
-
Pseudo-classes CSS
Styles pour les états de validation
- :valid { border-color: green; }
- :invalid { border-color: red; }
Exemple : formulaire avec validation complète
Code HTML avec validation
html
<form novalidate>
<div>
<label for="email">Email *</label>
<input
type="email"
id="email"
required
placeholder="nom@exemple.com"
>
</div>
<div>
<label for="password">Mot de passe *</label>
<input
type="password"
id="password"
required
minlength="8"
pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}"
title="Au moins 8 caractères avec majuscule, minuscule et chiffre"
>
</div>
<div>
<label for="age">Âge</label>
<input
type="number"
id="age"
min="13"
max="120"
>
</div>
<button type="submit">S'inscrire</button>
</form>
Formulaire fonctionnel
Bonnes pratiques
Créez des formulaires accessibles, ergonomiques et sécurisés qui offrent une excellente expérience utilisateur.
-
Accessibilité
-
Labels explicites
- ✅ Toujours associer : label[for] ↔ input[id]
- ❌ Éviter : placeholder comme seul label
-
Groupes et structure
- <fieldset> <legend>Informations personnelles</legend> </fieldset>
-
Messages d’erreur
- Messages clairs et constructifs
- Associés avec aria-describedby
- Focus sur le premier champ en erreur
- Couleurs + texte (pas que couleur)
-
-
UX/UI design
-
Progression logique
- Ordre de tabulation naturel
- Groupement par thématique
- Informations essentielles en premier
- Bouton principal bien visible
-
Feedback utilisateur
- .loading { cursor: wait; }
- .success { border-color: green; }
- .error { border-color: red; }
-
Mobile-friendly
- Types d’inputs appropriés (tel, email)
- Taille tactile suffisante (44px min)
- Espacement généreux
- Zoom activé sur les inputs
-
-
Sécurité
-
Validation côté serveur
⚠️ La validation HTML5 peut être contournée ! Toujours valider côté serveur.
- <form action="/secure-endpoint" method="POST">
-
Protection CSRF
- <input type="hidden" name="_token" value="csrf_token">
-
Données sensibles
- HTTPS obligatoire
- Autocomplete="off" pour mots de passe
- Ne pas stocker en plaintext
- Limiter les tentatives
-
-
Performance
-
Optimisation du DOM
- Éviter les formulaires trop complexes
- Pagination pour longs formulaires
- Lazy loading pour les selects
- Debounce pour validation en temps réel
-
Upload de fichiers
- <form enctype="multipart/form-data">
- <input type="file" accept=".jpg,.png" max-size="5MB">
-
Autocomplete intelligent
- <input autocomplete="email">
- <input autocomplete="current-password">
-
Travaux pratiques
Créez des formulaires professionnels et interactifs avec ces exercices progressifs.
-
Exercice 1 : formulaire de contact professionnel
Objectif
Créez un formulaire de contact complet avec validation, accessibilité et design soigné.
Spécifications
- Informations personnelles (nom, email, téléphone)
- Sujet avec liste déroulante pré-définie
- Message avec compteur de caractères
- Consentement RGPD obligatoire
- Validation complète et messages d’erreur
Aperçu attendu
Chaque champ a son label associé : c’est ce qui rend le formulaire utilisable au clavier et au lecteur d’écran. -
Exercice 2 : formulaire d’inscription avancé
Mission
Créez un formulaire d’inscription complexe avec tous les types d’inputs et une validation poussée.
Défis techniques
- Validation en temps réel avec JavaScript
- Upload d’avatar avec prévisualisation
- Sélecteur de date de naissance
- Choix multiples avec checkboxes
- Barre de progression du formulaire
Fonctionnalités attendues
Sept types d’input différents, tous natifs : le navigateur fournit déjà le sélecteur de date, le curseur et le sélecteur de couleur.
Code de démarrage
html
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Exercices - Formulaires HTML</title>
<style>
/* Styles de base pour les formulaires */
* { box-sizing: border-box; }
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
margin: 0;
padding: 20px;
background: #f5f7fa;
}
/* Conteneurs */
.container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
/* Formulaires */
form { margin-bottom: 30px; }
fieldset {
border: 2px solid #e1e8ed;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
legend {
font-weight: bold;
color: #2c3e50;
padding: 0 10px;
}
/* Labels et inputs */
label {
display: block;
margin-bottom: 5px;
font-weight: 500;
color: #34495e;
}
input, textarea, select {
width: 100%;
padding: 12px;
border: 2px solid #bdc3c7;
border-radius: 6px;
font-size: 16px;
transition: border-color 0.3s, box-shadow 0.3s;
}
input:focus, textarea:focus, select:focus {
outline: none;
border-color: #3498db;
box-shadow: 0 0 0 3px rgba(52,152,219,0.2);
}
/* États de validation */
input:valid { border-color: #27ae60; }
input:invalid:not(:focus):not(:placeholder-shown) { border-color: #e74c3c; }
/* Types spéciaux */
input[type="checkbox"], input[type="radio"] {
width: auto;
margin-right: 8px;
}
input[type="file"] {
border: none;
padding: 8px 0;
}
/* Boutons */
button {
background: #3498db;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s;
}
button:hover { background: #2980b9; }
button:disabled {
background: #bdc3c7;
cursor: not-allowed;
}
/* Layout */
.form-group { margin-bottom: 20px; }
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
/* Messages d'aide */
.help-text {
font-size: 14px;
color: #7f8c8d;
margin-top: 5px;
}
.error-message {
color: #e74c3c;
font-size: 14px;
margin-top: 5px;
}
/* Responsive */
@media (max-width: 768px) {
.form-row { grid-template-columns: 1fr; }
.container { margin: 10px; padding: 20px; }
}
</style>
</head>
<body>
<div class="container">
<h1>Exercices - Formulaires HTML</h1>
<!-- EXERCICE 1 : FORMULAIRE DE CONTACT -->
<section>
<h2>Exercice 1 : Formulaire de contact professionnel</h2>
<form action="/contact" method="POST" novalidate>
<!-- TODO: Champ token CSRF -->
<input type="hidden" name="_token" value="csrf_token_here">
<!-- TODO: Fieldset informations personnelles -->
<fieldset>
<legend>Informations personnelles</legend>
<div class="form-row">
<div class="form-group">
<label for="lastname">Nom *</label>
<input
type="text"
id="lastname"
name="lastname"
required
minlength="2"
maxlength="50"
autocomplete="family-name"
placeholder="Votre nom de famille"
>
</div>
<!-- TODO: Champ prénom -->
<div class="form-group">
<label for="firstname">Prénom</label>
<input
type="text"
id="firstname"
name="firstname"
maxlength="50"
autocomplete="given-name"
placeholder="Votre prénom"
>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="email">Email *</label>
<input
type="email"
id="email"
name="email"
required
autocomplete="email"
placeholder="nom@exemple.com"
>
</div>
<!-- TODO: Champ téléphone -->
<div class="form-group">
<label for="phone">Téléphone</label>
<input
type="tel"
id="phone"
name="phone"
pattern="[0-9]{10}"
autocomplete="tel"
placeholder="0123456789"
title="Numéro de téléphone à 10 chiffres"
>
<div class="help-text">Format: 10 chiffres sans espaces</div>
</div>
</div>
</fieldset>
<!-- TODO: Sujet du message -->
<div class="form-group">
<label for="subject">Sujet *</label>
<select id="subject" name="subject" required>
<option value="">Choisissez un sujet</option>
<option value="info">Demande d'information</option>
<option value="support">Support technique</option>
<!-- TODO: Ajoutez d'autres options -->
<option value="commercial">Question commerciale</option>
<option value="partnership">Partenariat</option>
<option value="other">Autre</option>
</select>
</div>
<!-- TODO: Message -->
<div class="form-group">
<label for="message">Message *</label>
<textarea
id="message"
name="message"
required
minlength="10"
maxlength="1000"
rows="5"
placeholder="Décrivez votre demande en détail..."
></textarea>
<div class="help-text">Minimum 10 caractères, maximum 1000</div>
</div>
<!-- TODO: Cases à cocher -->
<div class="form-group">
<label>
<input type="checkbox" name="newsletter" value="1">
Je souhaite recevoir la newsletter
</label>
</div>
<div class="form-group">
<label>
<input type="checkbox" name="terms" required>
J'accepte les <a href="/terms" target="_blank">conditions d'utilisation</a> *
</label>
</div>
<!-- TODO: Boutons d'action -->
<div class="form-group">
<button type="submit">Envoyer le message</button>
<button type="reset" style="margin-left: 10px; background: #95a5a6;">Effacer</button>
</div>
</form>
</section>
<hr style="margin: 40px 0;">
<!-- EXERCICE 2 : FORMULAIRE D'INSCRIPTION -->
<section>
<h2>Exercice 2 : Formulaire d'inscription avancé</h2>
<form action="/register" method="POST" enctype="multipart/form-data" novalidate>
<!-- TODO: Upload d'avatar -->
<div class="form-group" style="text-align: center;">
<label for="avatar">Photo de profil</label>
<div id="avatar-preview" style="width: 100px; height: 100px; border: 2px dashed #bdc3c7; border-radius: 50%; margin: 10px auto; display: flex; align-items: center; justify-content: center; background: #ecf0f1;">
📷
</div>
<input
type="file"
id="avatar"
name="avatar"
accept="image/jpeg,image/png,image/gif"
>
<div class="help-text">Formats acceptés: JPG, PNG, GIF (max 2MB)</div>
</div>
<!-- TODO: Informations de compte -->
<div class="form-row">
<div class="form-group">
<label for="username">Nom d'utilisateur *</label>
<input
type="text"
id="username"
name="username"
required
minlength="3"
maxlength="20"
pattern="[a-zA-Z0-9_-]{3,20}"
autocomplete="username"
placeholder="mon_nom_utilisateur"
>
<div class="help-text">3-20 caractères, lettres, chiffres, _ et - autorisés</div>
</div>
<div class="form-group">
<label for="register-email">Email *</label>
<input
type="email"
id="register-email"
name="email"
required
autocomplete="email"
placeholder="nom@exemple.com"
>
</div>
</div>
<!-- TODO: Mots de passe -->
<div class="form-row">
<div class="form-group">
<label for="password">Mot de passe *</label>
<input
type="password"
id="confirm-password"
name="confirm_password"
required
autocomplete="new-password"
>
<div id="password-match" class="help-text"></div>
</div>
</div>
<!-- TODO: Informations personnelles -->
<fieldset>
<legend>Informations personnelles</legend>
<div class="form-row">
<div class="form-group">
<label for="birthdate">Date de naissance</label>
<input
type="date"
id="birthdate"
name="birthdate"
min="1900-01-01"
max="2010-12-31"
autocomplete="bday"
>
</div>
<div class="form-group">
<label>Genre</label>
<div style="display: flex; gap: 15px; margin-top: 5px;">
<label>
<input type="radio" name="gender" value="M">
Homme
</label>
<label>
<input type="radio" name="gender" value="F">
Femme
</label>
<label>
<input type="radio" name="gender" value="O">
Autre
</label>
</div>
</div>
</div>
</fieldset>
<!-- TODO: Préférences et intérêts -->
<fieldset>
<legend>Préférences</legend>
<div class="form-group">
<label>Centres d'intérêt (plusieurs choix possibles)</label>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 10px;">
<label>
<input type="checkbox" name="interests[]" value="tech">
Technologie
</label>
<label>
<input type="checkbox" name="interests[]" value="design">
Design
</label>
<label>
<input type="checkbox" name="interests[]" value="business">
Business
</label>
<label>
<input type="checkbox" name="interests[]" value="marketing">
Marketing
</label>
<label>
<input type="checkbox" name="interests[]" value="education">
Éducation
</label>
<label>
<input type="checkbox" name="interests[]" value="health">
Santé
</label>
</div>
</div>
<div class="form-group">
<label for="experience">Niveau d'expérience</label>
<input
type="range"
id="experience"
name="experience"
min="0"
max="10"
value="5"
oninput="document.getElementById('experience-value').textContent = this.value"
>
<div class="help-text">
Années d'expérience: <span id="experience-value">5</span>
</div>
</div>
<div class="form-group">
<label for="theme-color">Couleur de thème préférée</label>
<input
type="color"
id="theme-color"
name="theme_color"
value="#3498db"
style="width: 60px; height: 40px; padding: 2px;"
>
</div>
</fieldset>
<!-- TODO: Acceptation des conditions -->
<div class="form-group">
<label>
<input type="checkbox" name="newsletter_signup" value="1">
Je souhaite recevoir des notifications par email
</label>
</div>
<div class="form-group">
<label>
<input type="checkbox" name="terms_register" required>
J'accepte les <a href="/terms" target="_blank">conditions d'utilisation</a> et la <a href="/privacy" target="_blank">politique de confidentialité</a> *
</label>
</div>
<!-- TODO: Boutons d'action -->
<div class="form-group">
<button type="submit" style="background: #27ae60;">Créer mon compte</button>
<button type="button" onclick="resetFormWithConfirm()" style="margin-left: 10px; background: #95a5a6;">Réinitialiser</button>
</div>
</form>
</section>
<!-- Instructions pour les étudiants -->
<section style="margin-top: 40px; padding: 20px; background: #f8f9fa; border-radius: 8px;">
<h2>Instructions pour les exercices</h2>
<h3>Exercice 1 - Points à améliorer :</h3>
<ul>
<li>Ajouter une validation en temps réel avec JavaScript</li>
<li>Implémenter des messages d'erreur personnalisés</li>
<li>Améliorer l'accessibilité avec aria-describedby</li>
<li>Ajouter un indicateur de progression</li>
<li>Tester la soumission et gérer les états de chargement</li>
</ul>
<h3>Exercice 2 - Défis avancés :</h3>
<ul>
<li>Implémenter la prévisualisation d'avatar en temps réel</li>
<li>Créer un indicateur de force de mot de passe dynamique</li>
<li>Ajouter la vérification de disponibilité du nom d'utilisateur</li>
<li>Implémenter une sauvegarde automatique en brouillon</li>
<li>Créer des animations de feedback pour les validations</li>
</ul>
<h3>Bonnes pratiques à appliquer :</h3>
<ul>
<li>Accessibilité : Labels, roles ARIA, navigation clavier</li>
<li>Performance : Debounce des validations, lazy loading</li>
<li>Sécurité : Validation côté serveur, protection CSRF</li>
<li>UX : Messages clairs, progression visuelle, états de chargement</li>
</ul>
</section>
</div>
<!-- JavaScript pour les fonctionnalités avancées -->
<script>
// Fonction de validation de démonstration
function showValidationDemo() {
const result = document.getElementById('validation-result');
result.className = 'mt-4 p-3 rounded bg-green-100 text-green-800';
result.textContent = '✅ Formulaire validé avec succès !';
result.classList.remove('hidden');
setTimeout(() => {
result.classList.add('hidden');
}, 3000);
}
// Fonction de réinitialisation avec confirmation
function resetFormWithConfirm() {
if (confirm('Êtes-vous sûr de vouloir effacer tous les champs ?')) {
document.querySelector('form[action="/register"]').reset();
// Réinitialiser les éléments personnalisés
document.getElementById('experience-value').textContent = '5';
document.getElementById('password-strength').innerHTML = 'Force du mot de passe: <span style="color: #e74c3c;">●○○○</span> Faible';
document.getElementById('password-match').textContent = '';
}
}
// Validation en temps réel pour les mots de passe
document.addEventListener('DOMContentLoaded', function() {
const password = document.getElementById('password');
const confirmPassword = document.getElementById('confirm-password');
const strengthIndicator = document.getElementById('password-strength');
const matchIndicator = document.getElementById('password-match');
if (password && strengthIndicator) {
password.addEventListener('input', function() {
const value = this.value;
let strength = 0;
let indicators = ['○', '○', '○', '○'];
let label = 'Très faible';
let color = '#e74c3c';
if (value.length >= 8) strength++;
if (/[a-z]/.test(value)) strength++;
if (/[A-Z]/.test(value)) strength++;
if (/[0-9]/.test(value)) strength++;
if (/[^A-Za-z0-9]/.test(value)) strength++;
for (let i = 0; i < strength && i < 4; i++) {
indicators[i] = '●';
}
if (strength >= 4) { label = 'Forte'; color = '#27ae60'; }
else if (strength >= 3) { label = 'Moyenne'; color = '#f39c12'; }
else if (strength >= 2) { label = 'Faible'; color = '#e67e22'; }
strengthIndicator.innerHTML = `Force du mot de passe: <span style="color: ${color}">${indicators.join('')}</span> ${label}`;
});
}
if (confirmPassword && matchIndicator) {
function checkPasswordMatch() {
const pass1 = password ? password.value : '';
const pass2 = confirmPassword.value;
if (pass2.length > 0) {
if (pass1 === pass2) {
matchIndicator.textContent = '✅ Les mots de passe correspondent';
matchIndicator.style.color = '#27ae60';
} else {
matchIndicator.textContent = '❌ Les mots de passe ne correspondent pas';
matchIndicator.style.color = '#e74c3c';
}
} else {
matchIndicator.textContent = '';
}
}
confirmPassword.addEventListener('input', checkPasswordMatch);
if (password) password.addEventListener('input', checkPasswordMatch);
}
// Prévisualisation d'avatar
const avatarInput = document.getElementById('avatar');
const avatarPreview = document.getElementById('avatar-preview');
if (avatarInput && avatarPreview) {
avatarInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(e) {
avatarPreview.innerHTML = `<img src="${e.target.result}" style="width: 100%; height: 100%; object-fit: cover; border-radius: 50%;">`;
};
reader.readAsDataURL(file);
}
});
}
});
</script>
</body>
</html>