Chapitre 68 : Gestion des états de formulaire avec :placeholder-shown et :disabled
Sélecteurs d'état spécifiques aux champs de saisie
En tant qu'architecte front-end senior, la gestion des formulaires ne se limite pas à la validation des données. Elle réside dans la capacité à fournir un feedback visuel instantané et une expérience utilisateur (UX) fluide. L'utilisation combinée des pseudo-classes :placeholder-shown et :disabled permet de créer des interfaces dynamiques, comme les "floating labels" (labels flottants), sans recourir à du JavaScript pour surveiller chaque événement de saisie.
Le sélecteur :placeholder-shown
1. Quoi
Le sélecteur :placeholder-shown est une pseudo-classe CSS qui cible un élément de formulaire (généralement <input> ou <textarea>) tant que le texte de remplacement (placeholder) est visible à l'écran.
Contrairement à ce que l'on pourrait penser, ce sélecteur ne détecte pas si l'attribut placeholder est présent dans le HTML, mais si le navigateur l'affiche réellement. Dès que l'utilisateur saisit un seul caractère, le placeholder disparaît, et l'état :placeholder-shown devient faux.
2. Pourquoi
Dans un projet professionnel, :placeholder-shown est l'outil fondamental pour implémenter des Floating Labels.
Le problème classique des placeholders est qu'ils disparaissent dès que l'utilisateur commence à écrire, faisant perdre le contexte du champ (ex: "Quel était déjà le nom de ce champ ?"). En utilisant :placeholder-shown, on peut déplacer le label vers le haut du champ dès que la saisie commence, maintenant ainsi l'accessibilité et la clarté visuelle sans ajouter de listeners onInput ou onChange en JavaScript.
3. Comment
A. Syntaxe de base
/* Style appliqué quand le placeholder est visible (champ vide) */
input:placeholder-shown {
border-color: gray;
background-color: #f9f9f9;
}
/* Style appliqué quand l'utilisateur a commencé à saisir (placeholder caché) */
input:not(:placeholder-shown) {
border-color: blue;
background-color: white;
}
B. Cas concret : Implémentation d'un Floating Label (Pattern Senior)
Pour réaliser un label flottant, on utilise souvent le sélecteur de frère adjacent (+) ou le sélecteur général (~). Le label est placé après l'input dans le DOM pour permettre au CSS de cibler le label en fonction de l'état de l'input.
<div class="input-group">
<input type="text" id="username" placeholder=" " required />
<label for="username">Nom d'utilisateur</label>
</div>
.input-group {
position: relative;
margin-bottom: 2rem;
}
.input-group input {
font-size: 1rem;
padding: 1rem;
border: 2px solid #ccc;
border-radius: 4px;
outline: none;
width: 100%;
transition: border-color 0.2s;
}
.input-group label {
position: absolute;
left: 1rem;
top: 1rem;
color: #999;
pointer-events: none; /* Permet de cliquer à travers le label pour focus l'input */
transition: all 0.2s ease-out;
background-color: white;
padding: 0 4px;
}
/*
LOGIQUE SENIOR :
Si l'input n'est PAS en état :placeholder-shown
OU s'il a le focus, on déplace le label.
*/
.input-group input:focus + label,
.input-group input:not(:placeholder-shown) + label {
top: -0.5rem;
left: 0.5rem;
font-size: 0.75rem;
color: #007bff;
}
.input-group input:focus {
border-color: #007bff;
}
C. Limitations
- L'attribut placeholder obligatoire : Pour que
:placeholder-shownfonctionne, l'élément doit avoir un attributplaceholder. Si vous ne voulez pas de texte visible, utilisez un espace vide :placeholder=" ". - Compatibilité : Bien que largement supporté, certains anciens navigateurs géraient mal la transition entre le focus et l'affichage du placeholder.
- Accessibilité (A11y) : Le label doit toujours être lié via l'attribut
foret l'id de l'input, car le déplacement visuel ne remplace pas la structure sémantique.
4. Zone de Danger
❌ Erreur commune : Utiliser :focus seul pour animer le label.
Problème : Dès que l'utilisateur quitte le champ (blur), le label redescend et recouvre le texte saisi, rendant le formulaire illisible.
✅ Bonne pratique : Combiner :focus et :not(:placeholder-shown).
Résultat : Le label monte au focus ET reste en haut tant que le champ contient du texte.
Cycle de vie des états de saisie
Le sélecteur :disabled
1. Quoi
La pseudo-classe :disabled cible tout élément de formulaire qui possède l'attribut HTML disabled. Cet état indique que l'élément est non interactif et que sa valeur ne sera pas envoyée lors de la soumission du formulaire.
2. Pourquoi
L'aspect visuel d'un champ désactivé est crucial pour l'UX. Un utilisateur ne doit jamais essayer de cliquer sur un champ qui ne répond pas. Le style :disabled doit être distinctement différent de l'état normal pour communiquer l'impossibilité d'interaction.
3. Comment
A. Syntaxe de base
input:disabled {
cursor: not-allowed;
opacity: 0.6;
background-color: #eee;
}
B. Cas concret : Gestion d'état complexe (Read-only vs Disabled)
Il est fréquent de confondre readonly et disabled.
disabled: Le champ est grisé, non focusable, et non envoyé avec le formulaire.readonly: Le champ est non modifiable, mais est envoyé avec le formulaire et reste focusable.
/* Style pour les champs non modifiables */
input[readonly] {
background-color: #fdfdfd;
border-style: dashed;
cursor: default;
}
/* Style pour les champs totalement désactivés */
input:disabled {
background-color: #e9ecef;
color: #6c757d;
cursor: not-allowed;
border-color: #dee2e6;
}
C. Limitations
- Héritage :
:disabledne s'applique qu'aux éléments de formulaire. Si vous avez undivqui agit comme un bouton, vous devrez utiliser un attribut personnalisé (ex:[data-disabled="true"]) et gérer le style manuellement. - Focus : Un élément
:disabledne peut pas recevoir le focus clavier (Tab), ce qui est correct sémantiquement mais peut être déroutant si l'utilisateur s'attend à pouvoir lire le contenu via un lecteur d'écran.
4. Zone de Danger
❌ Erreur commune : Se contenter de changer la couleur du texte en gris. Problème : Les utilisateurs malvoyants ou daltoniens peuvent ne pas percevoir la différence.
✅ Bonne pratique : Combiner plusieurs indices visuels : cursor: not-allowed, opacity, changement de couleur de fond et suppression des effets de survol (:hover).
/* Désactiver les effets de hover sur les champs disabled */
input:disabled:hover {
border-color: #dee2e6; /* On force la couleur d'origine pour éviter l'effet de survol */
box-shadow: none;
}
Questions clés
1. Quelle est la différence fondamentale entre :placeholder-shown et :focus ?
Découvrir la réponse
:focus indique que l'élément a la saisie active (le curseur clignote), tandis que :placeholder-shown indique que le champ est vide (le texte d'aide est visible). Un champ peut être focus sans que le placeholder soit montré (si l'utilisateur a déjà écrit quelque chose), ou être vide sans être focus.
2. Pourquoi utiliser placeholder=" " (un espace) au lieu de ne pas mettre de placeholder du tout ?
Découvrir la réponse
Parce que le sélecteur :placeholder-shown ne s'active que si l'attribut placeholder est présent. Si l'attribut est absent, le navigateur considère que le placeholder n'est jamais "montré", et :placeholder-shown sera toujours faux, cassant ainsi la logique des labels flottants.
3. Un élément :disabled est-il envoyé lors de la soumission d'un formulaire HTML standard ?
Découvrir la réponse
Non. Les champs marqués comme disabled sont ignorés lors de la sérialisation du formulaire. Si vous avez besoin qu'un champ soit non modifiable mais tout de même envoyé au serveur, vous devez utiliser l'attribut readonly.
4. Comment empêcher un label flottant de redescendre sur le texte quand le champ est désactivé ?
Découvrir la réponse
En ajoutant l'état :disabled à la règle de maintien du label :
.input-group input:not(:placeholder-shown) + label,
.input-group input:disabled + label {
top: -0.5rem;
}
Mise en pratique
Exercice 1 : Reproduction guidée
Objectif : Créer un champ de saisie simple où la bordure devient verte uniquement lorsque l'utilisateur a commencé à saisir du texte (le placeholder a disparu).
- Utilisez un
<input>avec unplaceholder. - Appliquez le style via
:not(:placeholder-shown).
Découvrir la solution commentée
<input type="text" placeholder="Saisissez votre nom..." class="custom-input" />
.custom-input {
border: 2px solid #ccc;
padding: 8px;
outline: none;
transition: border-color 0.3s;
}
/*
On cible l'état où le placeholder n'est PLUS affiché,
ce qui signifie que l'utilisateur a saisi au moins un caractère.
*/
.custom-input:not(:placeholder-shown) {
border-color: green;
}
Exercice 2 : Adaptation
Objectif : Modifier le pattern du Floating Label pour qu'il change de couleur en rouge si le champ est désactivé (disabled), tout en gardant le label en position haute.
- Reprenez la structure HTML du cours.
- Ajoutez un attribut
disabledà l'input. - Le label doit devenir rouge et rester en haut.
Découvrir la solution commentée
<div class="input-group">
<input type="text" id="email" placeholder=" " disabled />
<label for="email">Email (Désactivé)</label>
</div>
.input-group {
position: relative;
margin-top: 20px;
}
.input-group input {
padding: 10px;
border: 1px solid #ccc;
}
.input-group label {
position: absolute;
left: 10px;
top: 10px;
transition: 0.2s;
pointer-events: none;
}
/* Maintien du label en haut si focus, si texte présent OU si disabled */
.input-group input:focus + label,
.input-group input:not(:placeholder-shown) + label,
.input-group input:disabled + label {
top: -12px;
font-size: 12px;
}
/* Style spécifique quand l'input est disabled */
.input-group input:disabled {
background-color: #f5f5f5;
cursor: not-allowed;
}
.input-group input:disabled + label {
color: red;
}
Exercice 3 : Conception
Objectif : Créer un système de "Champ de Code de Validation" (4 chiffres).
- Chaque champ doit avoir un placeholder (ex: "0").
- Le label global "Code de validation" doit être visible au-dessus.
- Si l'un des champs est rempli, le label global doit changer de couleur (indice : utilisez
:has()si supporté, ou structurez vos inputs pour utiliser:placeholder-shown). - Si le formulaire est en mode "lecture seule" (via une classe
.is-lockedsur le parent), tous les inputs doivent passer en état:disabledvisuellement.
Propriétés CSS attendues :
| Sélecteur | Propriétés clés | Rôle |
|---|---|---|
.code-container | display: flex, flex-direction: column, gap, width | Conteneur principal vertical |
.main-label | font-weight: bold, transition: color | Label global |
.inputs-wrapper | display: flex, gap | Aligne les 4 champs en ligne |
.inputs-wrapper input | width: 40px, height: 40px, text-align: center, font-size | Style de chaque chiffre |
.code-container:has(input:not(:placeholder-shown)) .main-label | color: #007bff | Label bleu si au moins un chiffre saisi |
.is-locked input | background-color: #eee, cursor: not-allowed, pointer-events: none | Simulation du mode désactivé |
.is-locked .main-label | color: #999 | Label grisé en mode verrouillé |
Le sélecteur :has(input:not(:placeholder-shown)) agit comme une logique "OU" : si au moins un input parmi les quatre a du contenu (placeholder caché), le label change de couleur. La classe .is-locked sur le parent simule un mode désactivé visuel sans modifier le HTML de chaque input individuellement.
Découvrir la solution commentée
<div class="code-container is-locked">
<label class="main-label">Code de validation</label>
<div class="inputs-wrapper">
<input type="text" placeholder="0" maxlength="1" />
<input type="text" placeholder="0" maxlength="1" />
<input type="text" placeholder="0" maxlength="1" />
<input type="text" placeholder="0" maxlength="1" />
</div>
</div>
.code-container {
display: flex;
flex-direction: column;
gap: 10px;
width: 200px;
}
.main-label {
font-weight: bold;
transition: color 0.3s;
}
.inputs-wrapper {
display: flex;
gap: 5px;
}
.inputs-wrapper input {
width: 40px;
height: 40px;
text-align: center;
font-size: 1.2rem;
}
/*
LOGIQUE AVANCÉE :
Si le container contient au moins un input qui n'affiche PAS son placeholder,
on change la couleur du label.
*/
.code-container:has(input:not(:placeholder-shown)) .main-label {
color: #007bff;
}
/*
Gestion du mode verrouillé :
On simule l'état disabled via le parent .is-locked
*/
.is-locked input {
background-color: #eee;
cursor: not-allowed;
pointer-events: none; /* Empêche l'interaction */
}
/* On peut aussi utiliser le vrai attribut disabled en JS,
mais ici on gère la stylisation via la classe parente */
.is-locked .main-label {
color: #999;
}