Niveau 16

Les Formulaires

Maîtriser le stylisme des formulaires en CSS : tous les types d'inputs, composants custom, validation, accessibilité, et patterns de formulaires complets.

1. Le concept fondamental

En une phrase : Les formulaires en CSS se contrôlent à travers la réinitialisation des styles natifs, le stylisme ciblé de chaque élément, et la création de composants custom pour une expérience utilisateur fluide et accessible.

Les formulaires sont le pont entre l'utilisateur et l'application. Ils collectent des données, authentifient, filtrent, et interagissent. En CSS, on ne se contente pas de colorer les inputs : on réinitialise les styles natifs, on crée des composants custom (checkbox, radio, switch, range), on gère les états de validation, et on conçoit des layouts de formulaire accessibles et responsives. C'est le chapitre le plus complet de cette formation.

Les éléments d'un formulaire

Élément Rôle
<form> Conteneur principal du formulaire
<fieldset> + <legend> Groupe logique de champs avec étiquette
<label> Étiquette liée à un champ (accessibilité cruciale)
<input> Champ de saisie (18+ types différents)
<textarea> Zone de texte multi-lignes
<select> + <option> Liste déroulante
<button> Bouton d'action
<optgroup> Groupe d'options dans un select

Les types d'input

Type Description Rendu natif
textTexte simpleChamp texte
emailAdresse email (validation auto)Champ texte
passwordMot de passe (masqué)Champ masqué
numberNombre avec flèchesChamp numérique
telNuméro de téléphoneChamp texte
urlURL (validation auto)Champ texte
searchRecherche (reset possible)Champ avec X
dateSélecteur de dateInput date
timeSélecteur d'heureInput time
monthSélecteur mois/annéeInput month
weekSélecteur semaineInput week
datetime-localDate + heureInput datetime
colorSélecteur de couleurInput color
rangeCurseur (slider)Slider
fileSélection de fichierBouton Parcourir
checkboxCocher (multi)Case à cocher
radioSélection (unique)Bouton radio
hiddenChamp cachéInvisible

2. Syntaxe et propriétés

Réinitialisation avec appearance: none

/* Supprimer les styles natifs du navigateur */
input, select, textarea, button {
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
}

/* Pour les checkboxes et radios custom */
input[type="checkbox"],
input[type="radio"] {
  appearance: none;
  -webkit-appearance: none;
  width: 20px;
  height: 20px;
  border: 2px solid #94a3b8;
  border-radius: 4px;
  cursor: pointer;
}
Astuce : appearance: none est la propriété clé pour personnaliser les éléments de formulaire. Sans elle, les styles custom sont partiels ou incohérents selon les navigateurs.

Styliser les inputs texte

/* Style de base */
input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
input[type="tel"],
input[type="url"],
input[type="search"] {
  width: 100%;
  padding: 0.65rem 0.85rem;
  font-size: 1rem;
  font-family: inherit;
  color: #1e293b;
  background: white;
  border: 2px solid #e2e8f0;
  border-radius: 8px;
  outline: none;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

/* Focus */
input:focus {
  border-color: #2563eb;
  box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
}

/* Placeholder */
input::placeholder {
  color: #94a3b8;
  opacity: 1;
}

/* Désactivé */
input:disabled {
  background: #f1f5f9;
  color: #94a3b8;
  cursor: not-allowed;
}

/* Lecture seule */
input:read-only {
  background: #f8fafc;
}

L'importance du label

<!-- ✅ BON — Label associé via for/id -->
<label for="email">Adresse email</label>
<input type="email" id="email" name="email">

<!-- ✅ BON — Label imbriqué (association implicite) -->
<label>
  Nom complet
  <input type="text" name="nom">
</label>

<!-- ❌ MAUVAIS — Pas de label -->
<input type="text" placeholder="Entrez votre nom">

/* Style du label */
label {
  display: block;
  margin-bottom: 0.35rem;
  font-weight: 600;
  color: #1e293b;
  font-size: 0.9rem;
}

/* Indicateur requis */
label .required {
  color: #dc2626;
  margin-left: 0.15em;
}

États disabled/enabled

/* État désactivé */
input:disabled,
select:disabled,
textarea:disabled,
button:disabled {
  background: #f1f5f9;
  color: #94a3b8;
  cursor: not-allowed;
  opacity: 0.7;
  border-color: #e2e8f0;
}

/* État activé */
input:enabled {
  background: white;
  color: #1e293b;
}

/* Focus uniquement sur les champs activés */
input:focus:enabled {
  border-color: #2563eb;
  box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
}

Indicateurs requis/optionnel

/* Astérisque requis via ::after */
label.requis::after {
  content: " *";
  color: #dc2626;
}

/* Badge optionnel */
label.optionnel::after {
  content: " (optionnel)";
  font-weight: 400;
  color: #94a3b8;
  font-size: 0.85em;
}

États de validation

/* Champ valide */
input:valid {
  border-color: #16a34a;
}

/* Champ invalide */
input:invalid {
  border-color: #dc2626;
}

/* Hors de plage (number, range, date) */
input:out-of-range {
  border-color: #dc2626;
  background: #fef2f2;
}

/* Dans la plage */
input:in-range {
  border-color: #16a34a;
}

/* Quand le champ est touched (a été focus) */
input:user-valid {
  border-color: #16a34a;
}

input:user-invalid {
  border-color: #dc2626;
}

/* Message d'erreur sous le champ */
.error-message {
  color: #dc2626;
  font-size: 0.8rem;
  margin-top: 0.3rem;
  display: none;
}

input:invalid + .error-message {
  display: block;
}

/* Message de succès */
.success-message {
  color: #16a34a;
  font-size: 0.8rem;
  margin-top: 0.3rem;
  display: none;
}

input:valid + .success-message {
  display: block;
}
Astuce : Les pseudo-classes :user-valid et :user-invalid (nouveaux) ne s'appliquent qu'après que l'utilisateur a interagi avec le champ, évitant les erreurs affichées dès le chargement.

Styliser les selects

/* Select de base */
select {
  width: 100%;
  padding: 0.65rem 2.5rem 0.65rem 0.85rem;
  font-size: 1rem;
  font-family: inherit;
  color: #1e293b;
  background: white;
  border: 2px solid #e2e8f0;
  border-radius: 8px;
  outline: none;
  cursor: pointer;
  appearance: none;
  -webkit-appearance: none;

  /* Flèche custom */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%2364748b' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.75rem center;
  background-size: 12px;

  transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

select:focus {
  border-color: #2563eb;
  box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
}

select:disabled {
  background: #f1f5f9;
  color: #94a3b8;
  cursor: not-allowed;
}

Styliser les textareas

textarea {
  width: 100%;
  min-height: 120px;
  padding: 0.65rem 0.85rem;
  font-size: 1rem;
  font-family: inherit;
  color: #1e293b;
  background: white;
  border: 2px solid #e2e8f0;
  border-radius: 8px;
  outline: none;
  resize: vertical;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

textarea:focus {
  border-color: #2563eb;
  box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
}

/* Redimensionnement contrôlé */
textarea { resize: vertical; }   /* Vertical seulement */
textarea { resize: none; }       /* Pas de redimensionnement */
textarea { resize: both; }       /* Les deux directions */

Checkbox custom

/* Cacher l'input natif */
.checkbox-custom input[type="checkbox"] {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
}

/* Le label devient la checkbox */
.checkbox-custom label {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  cursor: pointer;
  font-size: 0.95rem;
}

/* La case visuelle */
.checkbox-custom .check-box {
  width: 22px;
  height: 22px;
  border: 2px solid #94a3b8;
  border-radius: 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.2s ease, border-color 0.2s ease;
  flex-shrink: 0;
}

/* Le check (coche) */
.checkbox-custom .check-mark {
  opacity: 0;
  transform: scale(0);
  transition: opacity 0.2s ease, transform 0.2s ease;
  color: white;
  font-size: 0.8rem;
  line-height: 1;
}

/* État coché */
.checkbox-custom input:checked + label .check-box {
  background: #2563eb;
  border-color: #2563eb;
}

.checkbox-custom input:checked + label .check-mark {
  opacity: 1;
  transform: scale(1);
}

/* Focus visible */
.checkbox-custom input:focus-visible + label .check-box {
  box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.3);
}

/* Désactivé */
.checkbox-custom input:disabled + label {
  opacity: 0.5;
  cursor: not-allowed;
}

Radio custom

.radio-custom input[type="radio"] {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
}

.radio-custom label {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  cursor: pointer;
}

.radio-custom .radio-circle {
  width: 22px;
  height: 22px;
  border: 2px solid #94a3b8;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  transition: border-color 0.2s ease;
}

.radio-custom .radio-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: #2563eb;
  transform: scale(0);
  transition: transform 0.2s ease;
}

.radio-custom input:checked + label .radio-circle {
  border-color: #2563eb;
}

.radio-custom input:checked + label .radio-dot {
  transform: scale(1);
}

.radio-custom input:focus-visible + label .radio-circle {
  box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.3);
}

Switch / Toggle custom

.switch-custom input[type="checkbox"] {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
}

.switch-custom label {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  cursor: pointer;
}

.switch-custom .switch-track {
  width: 48px;
  height: 26px;
  background: #cbd5e1;
  border-radius: 9999px;
  position: relative;
  transition: background 0.3s ease;
  flex-shrink: 0;
}

.switch-custom .switch-thumb {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 20px;
  height: 20px;
  background: white;
  border-radius: 50%;
  box-shadow: 0 1px 3px rgba(0,0,0,0.2);
  transition: transform 0.3s ease;
}

.switch-custom input:checked + label .switch-track {
  background: #2563eb;
}

.switch-custom input:checked + label .switch-thumb {
  transform: translateX(22px);
}

.switch-custom input:focus-visible + label .switch-track {
  box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.3);
}

Range slider custom

input[type="range"] {
  width: 100%;
  height: 6px;
  background: #e2e8f0;
  border-radius: 3px;
  outline: none;
  appearance: none;
  -webkit-appearance: none;
}

/* Thumb — WebKit (Chrome, Safari, Edge) */
input[type="range"]::-webkit-slider-thumb {
  appearance: none;
  -webkit-appearance: none;
  width: 20px;
  height: 20px;
  background: #2563eb;
  border-radius: 50%;
  cursor: pointer;
  border: 3px solid white;
  box-shadow: 0 1px 4px rgba(0,0,0,0.2);
  transition: transform 0.2s ease;
}

input[type="range"]::-webkit-slider-thumb:hover {
  transform: scale(1.15);
}

/* Thumb — Firefox */
input[type="range"]::-moz-range-thumb {
  width: 20px;
  height: 20px;
  background: #2563eb;
  border-radius: 50%;
  cursor: pointer;
  border: 3px solid white;
  box-shadow: 0 1px 4px rgba(0,0,0,0.2);
}

/* Track — WebKit */
input[type="range"]::-webkit-slider-runnable-track {
  height: 6px;
  background: #e2e8f0;
  border-radius: 3px;
}

/* Track — Firefox */
input[type="range"]::-moz-range-track {
  height: 6px;
  background: #e2e8f0;
  border-radius: 3px;
}

/* Progress bar fill (Firefox) */
input[type="range"]::-moz-range-progress {
  background: #2563eb;
  border-radius: 3px;
}

Bouton file upload custom

.file-upload {
  position: relative;
  display: inline-block;
}

.file-upload input[type="file"] {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
}

.file-upload-label {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.65rem 1.25rem;
  background: #f1f5f9;
  color: #475569;
  border: 2px dashed #94a3b8;
  border-radius: 8px;
  cursor: pointer;
  font-weight: 500;
  transition: background 0.2s ease, border-color 0.2s ease;
}

.file-upload-label:hover {
  background: #e2e8f0;
  border-color: #2563eb;
}

.file-upload input:focus-visible + .file-upload-label {
  box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.3);
  border-color: #2563eb;
}

Pattern : Labels flottants

.floating-label {
  position: relative;
}

.floating-label input {
  width: 100%;
  padding: 1.2rem 0.85rem 0.4rem;
  font-size: 1rem;
  border: 2px solid #e2e8f0;
  border-radius: 8px;
  background: white;
  outline: none;
  transition: border-color 0.2s ease;
}

.floating-label label {
  position: absolute;
  top: 50%;
  left: 0.85rem;
  transform: translateY(-50%);
  color: #94a3b8;
  font-size: 1rem;
  pointer-events: none;
  transition: all 0.2s ease;
}

/* Label qui monte */
.floating-label input:focus + label,
.floating-label input:not(:placeholder-shown) + label {
  top: 0.6rem;
  transform: translateY(0);
  font-size: 0.75rem;
  color: #2563eb;
  font-weight: 600;
}

.floating-label input:focus + label {
  color: #2563eb;
}

Groupes d'inputs

/* Input + icône */
.input-group {
  position: relative;
}

.input-group input {
  padding-left: 2.5rem;
}

.input-group .input-icon {
  position: absolute;
  left: 0.85rem;
  top: 50%;
  transform: translateY(-50%);
  color: #94a3b8;
  pointer-events: none;
}

/* Input + bouton */
.input-group-btn {
  display: flex;
}

.input-group-btn input {
  border-radius: 8px 0 0 8px;
  flex: 1;
}

.input-group-btn button {
  padding: 0.65rem 1.25rem;
  background: #2563eb;
  color: white;
  border: 2px solid #2563eb;
  border-radius: 0 8px 8px 0;
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
  transition: background 0.2s ease;
}

.input-group-btn button:hover {
  background: #1d4ed8;
  border-color: #1d4ed8;
}

Disposition du formulaire (Grid/Flex)

/* Layout grid pour les formulaires */
.form-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1.25rem;
  max-width: 600px;
  margin: 0 auto;
}

@media (min-width: 640px) {
  .form-grid {
    grid-template-columns: 1fr 1fr;
  }

  .form-grid .full-width {
    grid-column: 1 / -1;
  }
}

/* Séparateur entre sections */
.form-divider {
  grid-column: 1 / -1;
  border: none;
  border-top: 1px solid #e2e8f0;
  margin: 0.5rem 0;
}

/* Fieldset */
fieldset {
  border: 1px solid #e2e8f0;
  border-radius: 8px;
  padding: 1.5rem;
}

legend {
  font-weight: 700;
  padding: 0 0.5rem;
  color: #1e293b;
}

Style des messages d'erreur

/* Groupe de champ avec erreur */
.form-group.has-error input,
.form-group.has-error select,
.form-group.has-error textarea {
  border-color: #dc2626;
}

.form-group.has-error input:focus,
.form-group.has-error select:focus,
.form-group.has-error textarea:focus {
  box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.15);
}

/* Message d'erreur */
.form-error {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  margin-top: 0.35rem;
  color: #dc2626;
  font-size: 0.8rem;
  font-weight: 500;
}

/* Icône d'erreur dans l'input */
.form-group.has-error .input-wrapper::after {
  content: "⚠";
  position: absolute;
  right: 0.75rem;
  top: 50%;
  transform: translateY(-50%);
  color: #dc2626;
}

Barre de recherche

.search-bar {
  position: relative;
  max-width: 500px;
}

.search-bar input {
  width: 100%;
  padding: 0.75rem 1rem 0.75rem 2.75rem;
  font-size: 1rem;
  border: 2px solid #e2e8f0;
  border-radius: 9999px;
  background: white;
  outline: none;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

.search-bar input:focus {
  border-color: #2563eb;
  box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
}

.search-bar .search-icon {
  position: absolute;
  left: 1rem;
  top: 50%;
  transform: translateY(-50%);
  color: #94a3b8;
  pointer-events: none;
}

Pattern : Formulaire de connexion

.login-form {
  max-width: 400px;
  margin: 2rem auto;
  padding: 2rem;
  background: white;
  border-radius: 12px;
  box-shadow: 0 4px 24px rgba(0, 0, 0, 0.1);
}

.login-form h2 {
  text-align: center;
  margin-bottom: 1.5rem;
}

.login-form .form-group {
  margin-bottom: 1rem;
}

.login-form .form-actions {
  margin-top: 1.5rem;
}

.login-form .form-footer {
  text-align: center;
  margin-top: 1rem;
  font-size: 0.9rem;
}

.login-form .form-footer a {
  color: #2563eb;
  text-decoration: none;
}

.login-form .form-footer a:hover {
  text-decoration: underline;
}

Pattern : Formulaire d'inscription

.register-form {
  max-width: 600px;
  margin: 2rem auto;
  padding: 2rem;
  background: white;
  border-radius: 12px;
  box-shadow: 0 4px 24px rgba(0, 0, 0, 0.1);
}

.register-form .form-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1rem;
}

/* Indicateur de force du mot de passe */
.password-strength {
  height: 4px;
  border-radius: 2px;
  margin-top: 0.35rem;
  background: #e2e8f0;
  overflow: hidden;
}

.password-strength-fill {
  height: 100%;
  width: 0%;
  border-radius: 2px;
  transition: width 0.3s ease, background 0.3s ease;
}

.password-strength-fill.weak { width: 33%; background: #dc2626; }
.password-strength-fill.medium { width: 66%; background: #f59e0b; }
.password-strength-fill.strong { width: 100%; background: #16a34a; }

Pattern : Indicateur d'étapes

.steps-indicator {
  display: flex;
  justify-content: center;
  gap: 0;
  margin-bottom: 2rem;
}

.step-item {
  display: flex;
  align-items: center;
  gap: 0;
}

.step-circle {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  border: 2px solid #e2e8f0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 600;
  font-size: 0.85rem;
  color: #94a3b8;
  background: white;
  transition: all 0.3s ease;
}

.step-item.active .step-circle {
  border-color: #2563eb;
  background: #2563eb;
  color: white;
}

.step-item.completed .step-circle {
  border-color: #16a34a;
  background: #16a34a;
  color: white;
}

.step-line {
  width: 60px;
  height: 2px;
  background: #e2e8f0;
  margin: 0 0.5rem;
}

.step-item.completed .step-line {
  background: #16a34a;
}

Gestion du focus

/* Focus visible sur tous les éléments de formulaire */
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
button:focus-visible {
  outline: 2px solid #2563eb;
  outline-offset: 2px;
}

/* Focus ring personnalisé */
input:focus {
  border-color: #2563eb;
  box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
}

/* Réduire le mouvement pour les utilisateurs sensibles */
@media (prefers-reduced-motion: reduce) {
  input, select, textarea, button {
    transition: none;
  }
}

3. Visualisation

Types d'inputs

Les principaux types d'inputs HTML avec leur rendu natif

Composants custom

Checkbox, radio et switch avec styles custom

États de validation

valeur valide
:valid — bordure verte
email invalide
:invalid — bordure rouge
désactivé
:disabled — grisé

Les principaux états visuels des champs de formulaire

Labels flottants

Rempli (label en haut)
Vide (label centré)

Le label flotte au-dessus du champ quand il est rempli ou focus

Étapes d'un formulaire multi-étapes

3
4

Indicateur de progression : complété (✓), actif (3), à venir (4)

4. Exemples progressifs

Exemple 1 — Input texte de base

.input-base {
  width: 100%;
  padding: 0.65rem 0.85rem;
  font-size: 1rem;
  font-family: inherit;
  color: #1e293b;
  background: white;
  border: 2px solid #e2e8f0;
  border-radius: 8px;
  outline: none;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

.input-base:focus {
  border-color: #2563eb;
  box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
}

.input-base::placeholder {
  color: #94a3b8;
}

Rendu :

Exemple 2 — Input avec validation

/* Groupe de champ */
.form-field { margin-bottom: 1rem; }

.form-field label {
  display: block;
  margin-bottom: 0.35rem;
  font-weight: 600;
  font-size: 0.9rem;
}

.form-field input {
  /* styles de base */
}

.form-field input:invalid:not(:placeholder-shown) {
  border-color: #dc2626;
}

.form-field .error-msg {
  color: #dc2626;
  font-size: 0.8rem;
  margin-top: 0.3rem;
  display: none;
}

.form-field input:invalid:not(:placeholder-shown) + .error-msg {
  display: block;
}

Rendu :

Veuillez entrer une adresse email valide

Exemple 3 — Checkbox et Radio custom

/* Cacher l'input natif */
.custom-cb input[type="checkbox"] {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
}

/* Case visuelle */
.custom-cb .check-box {
  width: 22px;
  height: 22px;
  border: 2px solid #94a3b8;
  border-radius: 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all 0.2s ease;
}

/* État coché */
.custom-cb input:checked + label .check-box {
  background: #2563eb;
  border-color: #2563eb;
}

Rendu :

Choisissez vos centres d'intérêt
Expérience

Exemple 4 — Switch / Toggle

.switch-track {
  width: 48px;
  height: 26px;
  background: #cbd5e1;
  border-radius: 9999px;
  position: relative;
  transition: background 0.3s ease;
}

.switch-thumb {
  position: absolute;
  top: 3px; left: 3px;
  width: 20px; height: 20px;
  background: white;
  border-radius: 50%;
  box-shadow: 0 1px 3px rgba(0,0,0,0.2);
  transition: transform 0.3s ease;
}

input:checked + label .switch-track {
  background: #2563eb;
}

input:checked + label .switch-thumb {
  transform: translateX(22px);
}

Rendu :

Exemple 5 — Range slider custom

input[type="range"] {
  width: 100%;
  appearance: none;
  background: transparent;
}

input[type="range"]::-webkit-slider-runnable-track {
  height: 6px;
  background: #e2e8f0;
  border-radius: 3px;
}

input[type="range"]::-webkit-slider-thumb {
  appearance: none;
  width: 20px; height: 20px;
  background: #2563eb;
  border-radius: 50%;
  border: 3px solid white;
  box-shadow: 0 1px 4px rgba(0,0,0,0.2);
  margin-top: -7px;
  cursor: pointer;
}

Rendu :

Exemple 6 — File upload custom

Rendu :

Exemple 7 — Labels flottants

.floating input {
  padding: 1.2rem 0.85rem 0.4rem;
}

.floating label {
  position: absolute;
  top: 50%; left: 0.85rem;
  transform: translateY(-50%);
  transition: all 0.2s ease;
}

.floating input:focus + label,
.floating input:not(:placeholder-shown) + label {
  top: 0.6rem;
  transform: translateY(0);
  font-size: 0.75rem;
  color: #2563eb;
  font-weight: 600;
}

Rendu :

Exemple 8 — Input group (icône + bouton)

Rendu :

🔍

Exemple 9 — Barre de recherche

Rendu :

🔍

Exemple 10 — Select stylé

Rendu :

Exemple 11 — Textarea stylé

Rendu :

Exemple 12 — Formulaire de connexion

Rendu :

Exemple 13 — Formulaire de contact

Rendu :

Contactez-nous

Exemple 14 — Formulaire d'inscription

Rendu :

Créer un compte

Minimum 8 caractères avec majuscule, minuscule et chiffre

Exemple 15 — Indicateur de formulaire multi-étapes

Rendu :

3
4

Complété Complété En cours À venir

5. Erreurs fréquentes

Erreur n°1 — Ne pas utiliser de label

Les inputs sans <label> sont inaccessibles. Les lecteurs d'écran ne peuvent pas décrire le champ. Utilisez toujours for/id ou un label imbriqué.

/* ❌ MAUVAIS — Pas de label */
<input type="text" placeholder="Votre nom">

/* ✅ BON — Label associé */
<label for="nom">Nom complet</label>
<input type="text" id="nom" name="nom">
Erreur n°2 — Oublier appearance: none

Sans appearance: none, les styles CSS sont partiels et incohérents entre navigateurs. Les checkboxes, radios et selects gardent leur rendu natif.

/* ❌ MAUVAIS — Styles partiels */
select {
  border: 2px solid #2563eb;
  /* Le select garde son look natif sur Chrome */
}

/* ✅ BON — Réinitialisation complète */
select {
  appearance: none;
  border: 2px solid #2563eb;
  /* Contrôle total sur l'apparence */
}
Erreur n°3 — Afficher les erreurs dès le chargement

Les pseudo-classes :invalid et :valid s'appliquent dès le départ. Utilisez :user-invalid ou :not(:placeholder-shown) pour n'afficher les erreurs qu'après interaction.

/* ❌ MAUVAIS — Erreur affichée dès le chargement */
input:invalid { border-color: red; }

/* ✅ BON — Erreur uniquement après interaction */
input:user-invalid { border-color: red; }
/* Ou avec la classe JS */
input.invalid { border-color: red; }
Erreur n°4 — Supprimer l'outline sans alternative

outline: none sur les inputs casse l'accessibilité clavier. Toujours fournir un focus-visible personnalisé.

Erreur n°5 — Ne pas définir le type d'input

Oublier type="email" ou type="password" désactive les validations automatiques du navigateur et le clavier virtuel mobile.

Erreur n°6 — Utiliser placeholder au lieu de label

Le placeholder disparaît quand l'utilisateur tape. Le label est toujours visible. Le placeholder n'est pas un substitut au label.

Erreur n°7 — Ne pas gérer les états disabled

Les champs désactivés doivent avoir un style distinct (grisé, curseur not-allowed) pour indiquer qu'ils ne sont pas interactifs.

6. Bonnes pratiques

Bonne pratique n°1 — Toujours utiliser des labels

Chaque input doit avoir un label associé via for/id ou un label imbriqué. C'est une obligation d'accessibilité.

Bonne pratique n°2 — Réinitialiser avec appearance: none

Pour un contrôle total sur les styles, supprimez les styles natifs avant d'appliquer les vôtres.

Bonne pratique n°3 — Utiliser :focus-visible pour le focus

Montrez l'outline uniquement au focus clavier, pas au clic. C'est l'UX souhaitée pour la majorité des utilisateurs.

Bonne pratique n°4 — Afficher les erreurs après interaction

Utilisez :user-invalid ou des classes JS pour n'afficher les messages d'erreur qu'après que l'utilisateur a interagi avec le champ.

Bonne pratique n°5 — Respecter les contrastes WCAG

Les labels, placeholders et textes d'erreur doivent respecter les ratios de contraste minimum (4.5:1).

Bonne pratique n°6 — Utiliser fieldset/legend pour les groupes

Pour les groupes de radios ou checkboxes, <fieldset> et <legend> améliorent l'accessibilité.

Bonne pratique n°7 — Fournir des hints avec aria-describedby

Les indices sous les champs (format attendu, aide) doivent être liés via aria-describedby.

<div class="form-field">
  <label for="pass">Mot de passe</label>
  <input
    type="password"
    id="pass"
    aria-describedby="pass-hint"
    required
  >
  <p id="pass-hint" class="hint">
    Minimum 8 caractères
  </p>
</div>
Bonne pratique n°8 — Rendre les formulaires responsives

Utilisez CSS Grid ou Flexbox pour des layouts qui s'adaptent aux petits écrans. Les colonnes doubles deviennent simples sur mobile.

7. Mini-défi

Défi n°1 — Kit de composants formulaire

Objectif : Créer un kit complet de composants :

  • Input texte avec focus, placeholder, erreur
  • Select avec flèche custom
  • Textarea redimensionnable
  • Checkbox custom (hidden input + label + ::before/::after)
  • Radio custom
  • Switch/toggle
  • Chaque composant doit avoir ses états : normal, focus, disabled, error

Défi n°2 — Formulaire de profil

Objectif : Créer un formulaire de profil avec :

  • Champs en grille responsive (2 colonnes → 1 sur mobile)
  • Labels flottants sur au moins 2 champs
  • File upload pour l'avatar
  • Switches pour les préférences
  • Range slider pour un paramètre
  • Bouton de sauvegarde

Défi n°3 — Formulaire multi-étapes

Objectif : Créer un formulaire en 3 étapes avec :

  • Indicateur d'étapes visuel (cercles + lignes)
  • 3 groupes de champs différents par étape
  • Boutons Précédent / Suivant
  • Validation par étape
  • Le tout doit être accessible au clavier

8. Projet — Système de formulaires complet

Consigne :

Créez un système de formulaires complet qui combine tous les concepts vus dans ce chapitre :

  1. Un formulaire de connexion (email + password + se souvenir + forgot)
  2. Un formulaire d'inscription (prenom, nom, email, password, confirm, terms)
  3. Un formulaire de contact (nom, email, sujet, message)
  4. Un formulaire de paramètres (switches, ranges, selects, file upload)
  5. Un kit de composants custom (checkbox, radio, switch, range)
  6. Un indicateur de formulaire multi-étapes
  7. Un layout responsive (grid/flex)
  8. Une validation accessible (labels, aria-describedby, focus-visible)

9. Révision

Récapitulatif des concepts clés

Concept Propriétés / Éléments Usage principal
Réinitialisation appearance: none Supprimer les styles natifs
Labels <label for="id"> Accessibilité et association
Focus :focus-visible, outline, box-shadow Indicateur de focus accessible
Validation :valid, :invalid, :user-invalid Feedback visuel de validation
États :disabled, :enabled, :read-only Modification d'état visuel
Checkbox custom Hidden input + label + ::before/::after Case à cocher stylée
Radio custom Hidden input + label + ::before/::after Bouton radio stylé
Switch custom Hidden input + track + thumb Interrupteur on/off
Range custom ::-webkit-slider-thumb, ::-moz-range-thumb Curseur/slider stylé
Floating labels :not(:placeholder-shown), position: absolute Labels qui montent au focus
Groups fieldset, legend, flex/grid Groupement logique de champs
Layout CSS Grid, Flexbox Disposition responsive des formulaires
Accessibilité aria-describedby, aria-label, :focus-visible Accessibilité complète

Quiz rapide

  1. Pourquoi appearance: none est-il important pour les formulaires ?
  2. Quelle est la différence entre :invalid et :user-invalid ?
  3. Comment créer un checkbox custom avec CSS pur ?
  4. Pourquoi faut-il toujours utiliser des <label> ?
  5. Comment afficher un focus uniquement au clavier ?
  6. Quelle pseudo-classe permet de cibler les placeholders pour les labels flottants ?
  7. Comment créer un select avec une flèche custom ?
  8. Quel élément HTML groupe logiquement des champs de formulaire ?
Réponses :
  1. Il supprime les styles natifs du navigateur pour un contrôle total sur l'apparence
  2. :invalid s'applique dès le chargement, :user-invalid uniquement après interaction
  3. Input caché (opacity: 0) + label avec ::before/::after pour la case et la coche
  4. Pour l'accessibilité : les lecteurs d'écran en ont besoin pour décrire le champ
  5. Avec :focus-visible (et :focus:not(:focus-visible) pour masquer le focus clic)
  6. :not(:placeholder-shown) — le label monte quand le placeholder n'est plus visible
  7. Avec appearance: none + background-image SVG pour la flèche
  8. <fieldset> avec <legend> pour le titre du groupe

Cheat sheet

/* Réinitialisation globale formulaire */
input, select, textarea, button {
  appearance: none;
  font-family: inherit;
  font-size: 1rem;
}

/* Input texte */
.input {
  width: 100%;
  padding: 0.65rem 0.85rem;
  border: 2px solid #e2e8f0;
  border-radius: 8px;
  outline: none;
  transition: border-color 0.2s, box-shadow 0.2s;
}
.input:focus {
  border-color: #2563eb;
  box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
}
.input:placeholder { color: #94a3b8; }
.input:disabled { background: #f1f5f9; cursor: not-allowed; }
.input:user-invalid { border-color: #dc2626; }

/* Label */
label {
  display: block;
  margin-bottom: 0.35rem;
  font-weight: 600;
  font-size: 0.9rem;
}

/* Floating label */
.floating input { padding: 1.2rem 0.85rem 0.4rem; }
.floating label {
  position: absolute;
  top: 50%; left: 0.85rem;
  transform: translateY(-50%);
  transition: all 0.2s;
}
.floating input:focus + label,
.floating input:not(:placeholder-shown) + label {
  top: 0.6rem; transform: translateY(0);
  font-size: 0.75rem; color: #2563eb;
}

/* Checkbox custom */
.cb input { position: absolute; opacity: 0; }
.cb .box {
  width: 22px; height: 22px;
  border: 2px solid #94a3b8;
  border-radius: 6px;
  display: inline-flex; align-items: center; justify-content: center;
  transition: all 0.2s;
}
.cb input:checked + label .box {
  background: #2563eb; border-color: #2563eb;
}

/* Switch custom */
.sw input { position: absolute; opacity: 0; }
.sw .track {
  width: 48px; height: 26px;
  background: #cbd5e1; border-radius: 9999px;
  position: relative; transition: background 0.3s;
}
.sw .thumb {
  position: absolute; top: 3px; left: 3px;
  width: 20px; height: 20px;
  background: white; border-radius: 50%;
  transition: transform 0.3s;
}
.sw input:checked + label .track { background: #2563eb; }
.sw input:checked + label .thumb { transform: translateX(22px); }

/* Select */
select {
  width: 100%; padding: 0.65rem 2.5rem 0.65rem 0.85rem;
  border: 2px solid #e2e8f0; border-radius: 8px;
  background: white url("arrow.svg") no-repeat right 0.75rem center;
  cursor: pointer;
}

/* Range */
input[type="range"] {
  width: 100%; appearance: none;
  background: transparent;
}
input[type="range"]::-webkit-slider-thumb {
  appearance: none;
  width: 20px; height: 20px;
  background: #2563eb; border-radius: 50%;
  border: 3px solid white;
  box-shadow: 0 1px 4px rgba(0,0,0,0.2);
}

/* Focus accessible */
:focus-visible {
  outline: 2px solid #2563eb;
  outline-offset: 2px;
}
:focus:not(:focus-visible) { outline: none; }

/* Error */
.form-error { color: #dc2626; font-size: 0.8rem; }
.input:user-invalid { border-color: #dc2626; }