frontend / css
Pseudo-classes et pseudo-éléments avancés
Explication
Ce que vous allez apprendre
- Styler un élément parent selon son contenu avec
:has(), sans JavaScript - Factoriser des listes de sélecteurs répétitifs avec
:is()et:where() - Comprendre la différence de spécificité entre
:is()et:where() - Générer du contenu visuel en CSS pur avec
::before/::after,attr()etcounter() - Personnaliser la sélection de texte et les puces de liste avec
::selectionet::marker
Dans quel contexte ?
Un formulaire d'inscription doit afficher une bordure rouge sur le CONTENEUR d'un champ dès que son <input> interne devient invalide, et une carte produit doit adapter sa mise en page selon qu'elle contient ou non une image. Avant :has(), ces deux besoins imposaient d'ajouter/retirer des classes en JavaScript à chaque changement d'état ; cette leçon montre comment s'en passer entièrement.
Étape 1 : des besoins qui demandaient du JavaScript
Pendant longtemps, certains besoins de style conditionnel comme "styler un parent selon son contenu" nécessitaient forcément du JavaScript pour ajouter ou retirer des classes dynamiquement.
Étape 2 : la promesse de cette leçon
Les pseudo-classes récentes présentées ici (:has(), :is(), :where()) permettent d'exprimer directement en CSS pur des logiques qui demandaient auparavant du script.
Étape 3 : :has(), enfin un sélecteur parent
Commençons par la nouveauté la plus attendue : historiquement, CSS ne pouvait cibler QUE des descendants d'un élément, jamais l'inverse. :has() inverse cette limitation en stylant un élément SELON ce qu'il contient.
Étape 4 : un exemple concret de ce changement
On peut par exemple donner un layout différent à une carte selon qu'elle possède ou non une image — un changement de paradigme longtemps considéré comme impossible en CSS pur.
Astuce
:has() fonctionne aussi en combinaison avec des combinateurs : label:has(+ input:required)::after cible un label suivi d'un champ obligatoire, pour lui ajouter automatiquement un astérisque, sans toucher au HTML.
Étape 5 : :is() et :where(), factoriser sans répéter
Voyons maintenant deux pseudo-classes qui évitent de répéter un même sélecteur : au lieu d'écrire header a, main a, footer a séparément, on écrit :is(header, main, footer) a.
Étape 6 : la nuance cruciale entre les deux
Leur seule différence, mais elle compte : :is() prend la spécificité du sélecteur le plus fort de sa liste, alors que :where() a TOUJOURS une spécificité de zéro — utile pour poser des styles de base volontairement faciles à surcharger.
| Pseudo-classe | Spécificité résultante | Cas d'usage |
|---|---|---|
:is(header, main, footer) a | celle du sélecteur le plus fort de la liste | factoriser sans changer le comportement |
:where(.carte, .panneau) h2 | toujours zéro | styles de base facilement surchargeables |
Un dernier outil, le contenu généré
Il reste un outil souvent sous-exploité : ::before/::after combinés à content: attr(...) ou counter() génèrent du contenu visuel directement depuis le CSS, sans dupliquer d'information dans le HTML.
Et ensuite ?
Une fois ces sélecteurs maîtrisés, la prochaine étape aborde un sujet qu'on associe rarement au CSS : l'accessibilité visuelle, contraste et mouvement.
Commandes & code
Pseudo-classes et pseudo-éléments avancés
Les sélecteurs récents qui remplacent des kilos de JavaScript ou de classes utilitaires.
/* :is() : factorise une liste de sélecteurs, sans répétition */
:is(header, main, footer) a {
color: var(--couleur-primaire);
}
/* équivalent, sans :is(), à écrire 3 fois : header a, main a, footer a */
/* :where() : identique à :is(), mais spécificité TOUJOURS nulle */
:where(.carte, .panneau) h2 {
margin-top: 0; /* facilement surchargeable, même par un simple sélecteur de classe */
}
/* :not() accepte désormais une liste complète (niveau 4) */
.bouton:not(.desactive, .en-chargement) {
cursor: pointer;
}
/* combinaison réaliste */
:is(.carte, .modale):not(.compacte) .titre {
font-size: 1.25rem;
}/* :has() : le "sélecteur parent" enfin possible en CSS pur */
/* style un conteneur SELON ce qu'il contient */
.champ:has(input:invalid) {
border-color: red;
}
.carte:has(img) {
grid-template-columns: 120px 1fr; /* layout différent si la carte a une image */
}
/* Sélectionner un label qui précède un input requis, sans JS */
label:has(+ input:required)::after {
content: " *";
color: red;
}
/* Styler le formulaire entier selon l'état d'un champ précis */
form:has(#cgu:not(:checked)) button[type="submit"] {
opacity: 0.5;
pointer-events: none;
}/* ::before / ::after avancés : générer du contenu utile, pas juste décoratif */
.lien-externe::after {
content: "↗";
margin-left: 4px;
}
.requis::after {
content: attr(data-label); /* récupère la valeur d'un attribut du DOM */
}
.compteur {
counter-reset: etape;
}
.compteur li {
counter-increment: etape;
}
.compteur li::before {
content: "Étape " counter(etape) " : ";
font-weight: bold;
}
/* ::selection : personnalise la sélection de texte utilisateur */
::selection {
background: var(--couleur-primaire);
color: white;
}
/* ::backdrop : style l'arrière-plan d'un <dialog> ouvert ou du mode plein écran */
dialog::backdrop {
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
}
/* ::marker : personnalise les puces de liste, sans hack de list-style */
li::marker {
color: var(--couleur-primaire);
font-weight: bold;
}Résumé
:is()/:where()factorisent des sélecteurs répétitifs ; seule la spécificité résultante diffère.:has()permet enfin de styler un parent selon son contenu, sans JavaScript.content: attr(...)etcounter()génèrent du contenu dynamique directement en CSS.::backdropet::markercouvrent des cas historiquement impossibles à personnaliser proprement.
Exercices pratiques
Mission : un formulaire qui doit se surveiller lui-même
Objectif : Utiliser :has() pour remplacer une logique JavaScript de validation visuelle et choisir correctement entre :is() et :where().
Contexte
Sur inscription.css, un script JavaScript ajoute/retire actuellement une classe .champ-erreur sur le conteneur .champ dès qu'un <input> interne devient invalide, uniquement pour changer sa bordure en rouge. L'équipe veut supprimer ce script devenu inutile. Par ailleurs, le bouton d'envoi du formulaire doit rester visuellement désactivé (opacity: 0.5) tant que la case à cocher #cgu n'est pas cochée.