Maîtriser le stylisme des formulaires en CSS : tous les types d'inputs, composants custom, validation, accessibilité, et patterns de formulaires complets.
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.
| É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 |
| Type | Description | Rendu natif |
|---|---|---|
text | Texte simple | Champ texte |
email | Adresse email (validation auto) | Champ texte |
password | Mot de passe (masqué) | Champ masqué |
number | Nombre avec flèches | Champ numérique |
tel | Numéro de téléphone | Champ texte |
url | URL (validation auto) | Champ texte |
search | Recherche (reset possible) | Champ avec X |
date | Sélecteur de date | Input date |
time | Sélecteur d'heure | Input time |
month | Sélecteur mois/année | Input month |
week | Sélecteur semaine | Input week |
datetime-local | Date + heure | Input datetime |
color | Sélecteur de couleur | Input color |
range | Curseur (slider) | Slider |
file | Sélection de fichier | Bouton Parcourir |
checkbox | Cocher (multi) | Case à cocher |
radio | Sélection (unique) | Bouton radio |
hidden | Champ caché | Invisible |
/* 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;
}
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.
/* 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;
}
<!-- ✅ 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;
}
/* É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);
}
/* 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;
}
/* 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;
}
: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.
/* 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;
}
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 */
/* 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 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-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);
}
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;
}
.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;
}
.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;
}
/* 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;
}
/* 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;
}
/* 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;
}
.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;
}
.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;
}
.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; }
.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;
}
/* 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;
}
}
Les principaux types d'inputs HTML avec leur rendu natif
Checkbox, radio et switch avec styles custom
Les principaux états visuels des champs de formulaire
Le label flotte au-dessus du champ quand il est rempli ou focus
Indicateur de progression : complété (✓), actif (3), à venir (4)
.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 :
/* 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
/* 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 :
.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 :
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 :
Rendu :
.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 :
Rendu :
Rendu :
Rendu :
Rendu :
Rendu :
Rendu :
Rendu :
Minimum 8 caractères avec majuscule, minuscule et chiffre
Rendu :
Complété Complété En cours À venir
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">
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 */
}
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; }
outline: none sur les inputs casse l'accessibilité clavier. Toujours fournir un focus-visible personnalisé.
Oublier type="email" ou type="password" désactive les validations automatiques du navigateur et le clavier virtuel mobile.
Le placeholder disparaît quand l'utilisateur tape. Le label est toujours visible. Le placeholder n'est pas un substitut au label.
Les champs désactivés doivent avoir un style distinct (grisé, curseur not-allowed) pour indiquer qu'ils ne sont pas interactifs.
Chaque input doit avoir un label associé via for/id ou un label imbriqué. C'est une obligation d'accessibilité.
Pour un contrôle total sur les styles, supprimez les styles natifs avant d'appliquer les vôtres.
Montrez l'outline uniquement au focus clavier, pas au clic. C'est l'UX souhaitée pour la majorité des utilisateurs.
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.
Les labels, placeholders et textes d'erreur doivent respecter les ratios de contraste minimum (4.5:1).
Pour les groupes de radios ou checkboxes, <fieldset> et <legend> améliorent l'accessibilité.
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>
Utilisez CSS Grid ou Flexbox pour des layouts qui s'adaptent aux petits écrans. Les colonnes doubles deviennent simples sur mobile.
Défi n°1 — Kit de composants formulaire
Objectif : Créer un kit complet de composants :
Défi n°2 — Formulaire de profil
Objectif : Créer un formulaire de profil avec :
Défi n°3 — Formulaire multi-étapes
Objectif : Créer un formulaire en 3 étapes avec :
Consigne :
Créez un système de formulaires complet qui combine tous les concepts vus dans ce chapitre :
| 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 |
appearance: none est-il important pour les formulaires ?:invalid et :user-invalid ?<label> ?:invalid s'applique dès le chargement, :user-invalid uniquement après interactionopacity: 0) + label avec ::before/::after pour la case et la coche:focus-visible (et :focus:not(:focus-visible) pour masquer le focus clic):not(:placeholder-shown) — le label monte quand le placeholder n'est plus visibleappearance: none + background-image SVG pour la flèche<fieldset> avec <legend> pour le titre du groupe/* 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; }