Retour au cours

frontend / css

Pseudo-classes et pseudo-éléments avancés

Leçon 161 exercice

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() et counter()
  • Personnaliser la sélection de texte et les puces de liste avec ::selection et ::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-classeSpécificité résultanteCas d'usage
:is(header, main, footer) acelle du sélecteur le plus fort de la listefactoriser sans changer le comportement
:where(.carte, .panneau) h2toujours zérostyles 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.

css
/* :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;
}
css
/* :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;
}
css
/* ::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(...) et counter() génèrent du contenu dynamique directement en CSS.
  • ::backdrop et ::marker couvrent des cas historiquement impossibles à personnaliser proprement.

Exercices pratiques

1 disponible
1

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.

Résoudre l’exercice →