Skip to main content

Chapitre 31 : Le sélecteur :has() : le chaînon manquant pour le style conditionnel

Sélection d'éléments parents en fonction de l'état ou de la présence de leurs enfants

Pendant des décennies, le CSS a fonctionné selon un flux unidirectionnel : on pouvait styliser un enfant en fonction de son parent, ou un élément suivant en fonction d'un élément précédent (via le combinateur + ou ~), mais il était strictement impossible de styliser un parent en fonction de ce qui se passait à l'intérieur de celui-ci.

Pour accomplir cela, nous devions systématiquement recourir à JavaScript pour ajouter une classe .is-active ou .has-error sur le conteneur. Le sélecteur :has() change radicalement la donne en introduisant ce qu'on appelle le "sélecteur de parent" (parent selector).

Le fonctionnement du sélecteur :has()

1. Quoi

Le sélecteur :has() est une pseudo-classe relationnelle. Elle permet de cibler un élément si celui-ci contient un autre élément correspondant au sélecteur passé en argument.

Contrairement aux sélecteurs classiques qui descendent dans l'arbre DOM, :has() regarde "vers le bas" pour décider si l'élément "actuel" (le parent) doit être stylisé. Il ne stylise jamais l'élément à l'intérieur des parenthèses, mais toujours l'élément sur lequel la pseudo-classe est appliquée.

2. Pourquoi

L'utilité de :has() en milieu professionnel est immense car elle permet de supprimer des dizaines de lignes de JavaScript "colle" (glue code) dont le seul but était de synchroniser l'état visuel d'un composant.

Voici des cas d'usage concrets :

  • Validation de formulaires : Styliser un champ de saisie (le parent div) en rouge si l'input à l'intérieur est invalide.
  • Cartes interactives : Modifier l'apparence d'une carte entière lorsque l'utilisateur survole un bouton spécifique à l'intérieur de celle-ci.
  • Mises en page dynamiques : Masquer un titre de section si celle-ci ne contient aucun contenu (éléments enfants).
  • Gestion d'états complexes : Appliquer un style à un conteneur si une checkbox spécifique est cochée.

3. Comment

A. Syntaxe de base

La syntaxe est simple : SélecteurParent:has(SélecteurEnfant).

/* Cible le <div> seulement s'il contient un <img> */
div:has(img) {
border: 2px solid blue;
}

B. Cas concret : Le composant de carte interactive

Imaginons une carte de produit. Nous voulons que la carte entière s'illumine et que son ombre s'accentue, mais uniquement lorsque l'utilisateur survole le bouton "Ajouter au panier".

/* Structure HTML attendue : 
<article class="product-card">
<h3>Produit X</h3>
<button class="btn-add">Ajouter</button>
</article>
*/

.product-card {
padding: 1rem;
border: 1px solid #ddd;
transition: all 0.3s ease;
background: white;
}

/*
Cible .product-card SI elle contient un .btn-add
qui est actuellement survolé (:hover)
*/
.product-card:has(.btn-add:hover) {
border-color: #3498db;
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
transform: translateY(-5px);
background-color: #f8fbff;
}

C. Limitations et nuances

Bien que puissant, :has() possède quelques règles importantes :

  1. Pas de cycles : Vous ne pouvez pas créer de dépendances circulaires (ex: :has() qui cible un élément qui lui-même utilise :has() pour cibler le premier).
  2. Performance : Le navigateur doit potentiellement recalculer le style du parent dès qu'un enfant change. Sur des pages avec des milliers d'éléments imbriqués, un usage abusif de sélecteurs trop complexes dans :has() peut impacter les performances de rendu.
  3. Sélecteurs interdits : On ne peut pas utiliser de pseudo-éléments (comme ::before ou ::after) à l'intérieur de :has(), car ils ne font pas partie du DOM réel.

4. Zone de Danger

L'erreur de cible : Croire que :has() stylise l'enfant.

/* MAUVAIS : On pense styliser l'input, mais on stylise le form */
form:has(input:focus) {
background: yellow;
}
/* Résultat : Le formulaire devient jaune, pas l'input. */

La bonne pratique : Combiner :has() avec d'autres sélecteurs pour cibler précisément.

/* CORRECT : Le formulaire devient jaune, ET on peut aussi styliser l'input */
form:has(input:focus) {
background: #fffbe6;
}

form:has(input:focus) input {
outline: 2px solid orange;
}

Logique de fonctionnement du sélecteur :has()

Questions clés

1. Quelle est la différence fondamentale entre :has() et un sélecteur de descendant classique ?

Découvrir la réponse

Un sélecteur de descendant (ex: .parent .enfant) cible l'élément enfant. Le sélecteur :has() (ex: .parent:has(.enfant)) cible l'élément parent. C'est une inversion complète de la direction de sélection.

2. Peut-on utiliser :has() pour styliser un élément en fonction d'un élément frère (sibling) ?

Découvrir la réponse

Oui. Bien que :has() soit souvent utilisé pour les parents, on peut utiliser des combinateurs de frères à l'intérieur. Par exemple, div:has(+ p) cible un div qui est immédiatement suivi d'un paragraphe.

3. :has() est-il supporté par tous les navigateurs ?

Découvrir la réponse

Il est désormais supporté par toutes les versions modernes de Chrome, Firefox et Safari (Baseline 2023). Pour des projets supportant d'anciennes versions (IE11, vieux Edge), une solution de repli en JavaScript reste nécessaire.

4. Est-il possible d'enchaîner plusieurs conditions dans :has() ?

Découvrir la réponse

Oui, on peut utiliser la virgule pour un "OU" ou juxtaposer les conditions pour un "ET".

  • :has(.error, .warning) : Cible le parent si l'un ou l'autre est présent.
  • :has(.error):has(.critical) : Cible le parent seulement si les deux sont présents.

Mise en pratique

Exercice 1 : Reproduction guidée

Objectif : Créer un système de "carte de profil" qui change de couleur de bordure lorsque le badge "En ligne" est présent.

HTML de départ :

<!-- Carte avec badge "En ligne" : la bordure doit être verte -->
<div class="profile-card">
<p>Alice Martin</p>
<span class="status-badge online">En ligne</span>
</div>

<!-- Carte sans badge "online" : bordure grise par défaut -->
<div class="profile-card">
<p>Bob Dupont</p>
<span class="status-badge">Hors ligne</span>
</div>
Attention aux classes cumulées

Le sélecteur .status-badge.online (sans espace) cible un élément qui possède les deux classes simultanément. Dans le HTML, cela correspond à class="status-badge online". Un espace entre les deux classes dans le HTML suffit pour que les deux soient appliquées à l'élément.

Consignes :

  1. Appliquez une bordure 2px solid gray par défaut à .profile-card.
  2. Utilisez :has() pour que la bordure devienne verte uniquement si la carte contient un .status-badge qui possède aussi la classe .online.
Découvrir la solution commentée
.profile-card {
border: 2px solid gray;
padding: 10px;
display: inline-block;
}

/*
On cible la carte SI elle contient un élément qui a
à la fois la classe .status-badge ET la classe .online.
HTML attendu : <span class="status-badge online">
*/
.profile-card:has(.status-badge.online) {
border-color: green;
}

Exercice 2 : Adaptation

Objectif : Améliorer un formulaire de contact.

HTML de départ :

<form>
<div class="field-group">
<label for="email">Email</label>
<input type="email" id="email" required placeholder="votre@email.com" />
</div>
<div class="field-group">
<label for="nom">Nom (min 3 caractères)</label>
<input type="text" id="nom" required minlength="3" placeholder="Votre nom" />
</div>
</form>

CSS de départ (à adapter) :

.field-group {
padding: 1rem;
}

label {
display: block;
margin-bottom: 0.25rem;
}

input {
display: block;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
width: 100%;
}

Consignes :

  1. Utilisez :has(input:invalid) sur .field-group pour lui appliquer un fond légèrement rouge lorsque l'input interne est invalide.
  2. Bonus : Toujours via :has() sur le parent, faites passer le <label> en gras et en rouge quand l'input est invalide.
Découvrir la solution commentée
.field-group {
padding: 1rem;
transition: background 0.3s;
}

/* Stylise le conteneur si l'input interne est invalide */
.field-group:has(input:invalid) {
background-color: #fff5f5;
}

/* Stylise le label en descendant du parent qui a été validé par :has() */
.field-group:has(input:invalid) label {
font-weight: bold;
color: red;
}

Exercice 3 : Conception (Niveau Intermédiaire/Avancé)

Objectif : Créer un menu de navigation "intelligent".

HTML de départ :

<ul class="nav-list">
<li class="nav-item"><a href="#">Accueil</a></li>
<li class="nav-item">
<a href="#">Produits</a>
<ul>
<li><a href="#">Catégorie 1</a></li>
<li><a href="#">Catégorie 2</a></li>
</ul>
</li>
<li class="nav-item"><a href="#">Contact</a></li>
<li class="nav-item">
<a href="#">Services</a>
<ul>
<li><a href="#">Service A</a></li>
<li><a href="#">Service B</a></li>
</ul>
</li>
</ul>

Consignes :

  1. Affichez un sous-menu (ul imbriqué) masqué par défaut (display: none), visible au :hover du <li> parent.
  2. Utilisez :has(ul)::after sur .nav-item pour ajouter une icône uniquement aux éléments qui possèdent un sous-menu.
  3. Utilisez :has(ul:hover) pour changer la couleur de fond du .nav-item parent lorsque son sous-menu est survolé.
Découvrir la solution commentée
/* Structure : li > a + ul (sous-menu) */

.nav-item {
position: relative;
list-style: none;
}

/*
On ajoute la flèche uniquement aux éléments
qui possèdent un enfant <ul> (sous-menu)
*/
.nav-item:has(ul)::after {
content: ' ▼';
font-size: 0.8rem;
}

/*
On change le fond du li parent
quand le sous-menu (ul) est survolé
*/
.nav-item:has(ul:hover) {
background-color: #e0e0e0;
}

/* Style basique pour le sous-menu pour la démo */
.nav-item ul {
display: none;
}

.nav-item:hover ul {
display: block;
}