CSS :has() - Révolutionnez votre CSS avec le sélecteur parent

Xavier Moreau .

31 mars 2026

Extrait de code CSS montrant la pseudo-classe `:has()` pour styliser des images dans un élément `figure` qui contient une `figcaption`.

Le terme css has renvoie en pratique à la pseudo-classe relationnelle :has(), l’une des rares évolutions CSS qui change vraiment la manière de structurer une interface. Elle permet de styliser un parent ou un élément précédent en fonction de ce qu’il contient, ce qui simplifie des cas de formulaires, de cartes, de menus et d’états visuels conditionnels. Dans cet article, je vais aller droit à l’essentiel: comment cela fonctionne, où l’utiliser sans alourdir la feuille de style, et quelles limites connaître avant de l’adopter en production.

Les points essentiels à garder en tête

  • :has() permet de styliser un élément à partir de son contenu, sans ajouter de classe artificielle dans le HTML.
  • Elle sert aussi pour des relations de voisinage, par exemple un titre suivi d’un paragraphe ou d’un sous-titre.
  • Les sélecteurs trop larges, comme body:has(...), peuvent coûter cher sur des pages très dynamiques.
  • Le support navigateur est large en 2026, mais un fallback reste utile si vous ciblez encore des environnements anciens.
  • En frontend, les meilleurs usages concernent surtout les formulaires, les cartes, les menus et les layouts conditionnels.

Ce que change réellement la pseudo-classe relationnelle

Le gain principal est simple: on ne se bat plus contre la cascade pour remonter l’arbre DOM. Avec :has(), le sélecteur s’écrit autour de l’élément à styliser, puis on décrit la condition à l’intérieur. Cela ouvre enfin la porte à des règles du type form:has(input:invalid) ou h2:has(+ p), sans ajouter une classe de secours juste pour le style. Je l’utilise surtout quand l’interface doit réagir à une structure réelle, pas à un état métier complexe.

form:has(input:invalid) {
  border: 1px solid #d33;
  background: #fff5f5;
}

Ce n’est pas un sélecteur magique qui remplace JavaScript dans tous les cas. Il sert avant tout à exprimer une relation visuelle directe, lisible par le navigateur et facile à maintenir par l’équipe. Pour que cette logique reste saine, il faut ensuite comprendre la syntaxe exacte et les règles de compatibilité du sélecteur.

Écrire des sélecteurs lisibles et prévisibles

La forme générale est A:has(B) : A est l’ancre, B est la condition. MDN rappelle que la pseudo-classe prend la spécificité de son argument le plus spécifique, ce qui peut surprendre si vous l’empilez avec des classes utilitaires. En pratique, j’essaie de garder l’ancre courte et explicite, puis je limite le test intérieur avec des combinateurs comme > ou + pour éviter de parcourir tout le sous-arbre.

.card:has(> img) {
  grid-template-columns: 160px 1fr;
}

h2:has(+ p) {
  margin-bottom: 0.25rem;
}

:is(h1, h2, h3):has(+ p) {
  margin-bottom: 0.25rem;
}

Deux règles de prudence comptent vraiment. D’abord, :has() ne se niche pas à l’intérieur d’un autre :has(). Ensuite, les pseudo-éléments ne sont pas valides dans son argument, donc inutile d’essayer de viser un ::before ou un ::after. Quand je dois combiner plusieurs options, j’utilise plutôt :is() ou :where() autour des morceaux qui doivent rester lisibles ou moins spécifiques. C’est cette discipline qui évite la feuille de style opaque.

Une fois ces règles posées, le vrai intérêt apparaît dans les composants concrets du frontend.

Exemple CSS : `.card:has(.card_image)` s'applique si un élément `.card` contient un `.card_image`.

Cas d’usage frontend qui valent vraiment le coup

Je réserve :has() aux cas où l’interface gagne réellement en clarté. Les meilleurs exemples sont ceux où l’état visuel dépend d’un contenu déjà présent dans le DOM, pas d’une logique JavaScript supplémentaire. Sur un projet frontend, les usages les plus rentables sont souvent ceux-ci.

Cas Sélecteur Ce que cela apporte
Formulaire avec erreur form:has(input:invalid) On met en avant tout le bloc dès qu’un champ est invalide, ce qui améliore la lecture des erreurs.
Carte avec média .card:has(> img) On adapte la grille, l’espacement ou l’alignement si l’image est réellement présente.
Navigation active nav:has([aria-current="page"]) On met en évidence le conteneur de navigation sans ajouter d’état décoratif inutile.
Dialogue ouvert body:has(dialog[open]) On peut déclencher un style global, par exemple un fond assombri ou un verrouillage de défilement, avec prudence.
Titre suivi d’un paragraphe h2:has(+ p) On réduit l’espace sous un titre quand il introduit immédiatement un texte, ce qui donne un rythme plus compact.

Le dernier cas est le plus délicat: sur une racine comme body, le sélecteur reste lisible, mais il coûte plus cher si votre page change souvent. Je le garde seulement pour des états rares, comme l’ouverture d’un modal, pas pour des mutations permanentes. Quand on met ces cas face aux autres pseudo-classes, la frontière devient plus nette.

Ce que :has() fait mieux que les autres pseudo-classes

La confusion la plus fréquente, c’est de mélanger relation structurelle et état d’interaction. :has() observe le contenu ou le voisinage; :focus-within observe le focus; :is() et :where() servent surtout à regrouper des sélecteurs. Les comparer proprement évite des choix bancals.

Pseudo-classe Rôle principal Quand je la choisis Limite à connaître
:has() Relation structurelle Quand le parent doit réagir au contenu, à un enfant ou à un voisin Peut coûter plus cher et demande un sélecteur bien cadré
:focus-within État de focus Pour les formulaires, les menus ou les composants accessibles Ne regarde que le focus, pas la structure globale
:is() Regroupement Pour réduire la répétition et simplifier des sélecteurs longs Ne conditionne pas un parent à du contenu interne
:where() Regroupement à spécificité nulle Pour les couches utilitaires, les thèmes ou les bases CSS souples Très pratique, mais elle n’exprime pas une relation
:not() Négation Pour exclure un cas précis, pas pour détecter une présence Ne remplace pas une sélection “contient”

Quand j’ai besoin d’un état visuel basé sur la structure, :has() gagne. Si l’état vient d’un focus ou d’une interaction utilisateur, :focus-within est souvent plus direct et plus simple. Pour grouper sans surcharge, :is() et :where() restent plus légers; et si je veux juste exclure un cas, :not() suffit. Le point suivant, que je vérifie toujours avant de l’intégrer, c’est le support navigateur et le coût de recalcul.

Compatibilité et performances en 2026

Selon Can I use, le support mondial atteint 92,4 % en mai 2026. Chrome 105+, Edge 105+, Firefox 121+, Safari 15.4+ et leurs versions mobiles récentes le gèrent; Internet Explorer, lui, n’a jamais été compatible. En clair: pour la majorité des produits modernes, le support est suffisant, mais pas assez universel pour ignorer un fallback si vous avez encore une part d’audience ancienne.

Navigateur Support Lecture pratique
Chrome 105+ Oui Exploitation sans problème sur la base Chrome moderne
Firefox 121+ Oui Support stable pour les usages front-end courants
Safari 15.4+ Oui Couverture correcte sur macOS et iOS récents
Edge 105+ Oui Aligné sur Chromium pour les environnements entreprise récents
Opera et navigateurs mobiles modernes Oui dans les versions récentes Compatible pour la plupart des projets web contemporains
Internet Explorer Non Fallback indispensable si vous devez encore le servir
@supports selector(:has(a)) {
  .card:has(> img) {
    gap: 1rem;
  }
}

@supports not selector(:has(a)) {
  .card.has-media {
    gap: 1rem;
  }
}

Je m’en sers comme d’une porte de progression, pas comme d’un passe-partout. Côté performance, je respecte trois règles simples: ancre précise, test intérieur court, et pas de dépendance à des sélecteurs trop globaux comme *, :root ou body sauf nécessité réelle. MDN insiste d’ailleurs sur le fait que les ancres trop larges et les traversées profondes du sous-arbre peuvent coûter cher lors des mutations du DOM. C’est souvent là que les projets se dégradent: pas dans la syntaxe, mais dans les abus.

Les erreurs que je vois le plus souvent

  • Utiliser un ancrage trop large comme body:has(.x) pour un état qui devrait vivre sur un composant local.
  • Parcourir trop profondément avec des sélecteurs internes du type .card:has(.a .b .c).
  • Oublier la spécificité et se retrouver avec une règle plus forte que prévu dans la cascade.
  • Essayer de remplacer la logique produit par une condition purement CSS alors qu’un état JavaScript serait plus clair.
  • Ignorer les fallbacks alors qu’un simple @supports ou une classe de secours suffit.

Mon test mental est simple: si la règle peut être lue en une phrase courte et comprise sans relire le HTML, elle a des chances de rester saine. Si elle demande déjà trois exceptions et deux contournements, je la considère comme un mauvais candidat pour :has(). Il me reste alors à trancher avec une règle pratique, pas théorique.

La règle que j’applique avant de la garder en production

Je garde :has() quand la condition est visuelle, stable et proche du composant concerné. Je la retire dès qu’elle dépend d’un état applicatif, qu’elle impose un ancêtre trop général ou qu’elle complique un fallback inutilement. En production, c’est un excellent outil pour réduire le bruit des classes et rendre la cascade plus expressive, à condition de le traiter comme un scalpel, pas comme un substitut universel au JavaScript.

Si je démarre un nouveau composant, mon approche est simple: je tente d’abord une règle courte, je vérifie le rendu dans les navigateurs cibles, puis j’ajoute un fallback seulement si l’impact utilisateur le justifie. Cette discipline donne souvent une feuille de style plus propre que les solutions surchargées de classes d’état, et c’est précisément là que :has() mérite sa place.

Questions fréquentes

La pseudo-classe :has() permet de styliser un élément (parent ou précédent) en fonction de son contenu ou de ses éléments frères. Elle révolutionne la manière de structurer les interfaces en permettant des sélecteurs conditionnels basés sur la présence d'autres éléments, sans JavaScript.
Le principal avantage est de pouvoir styliser un élément en fonction de ses descendants ou voisins, sans ajouter de classes artificielles au HTML. Cela simplifie la gestion des formulaires, des cartes, des menus et des états visuels conditionnels, rendant le CSS plus lisible et maintenable.
Les cas d'usage les plus rentables incluent les formulaires avec champs invalides (form:has(input:invalid)), les cartes avec ou sans image (.card:has(> img)), les navigations actives, ou encore l'ajustement de l'espacement sous un titre suivi d'un paragraphe (h2:has(+ p)).
Évitez les ancrages trop larges (body:has(...)) et les sélecteurs internes trop profonds qui peuvent impacter les performances. :has() ne se niche pas et les pseudo-éléments ne sont pas valides dans son argument. Un fallback est conseillé pour les navigateurs plus anciens.
En mai 2026, :has() est largement supporté par les navigateurs modernes (Chrome 105+, Firefox 121+, Safari 15.4+, Edge 105+), atteignant environ 92,4% de compatibilité mondiale. Un fallback via @supports est recommandé si vous ciblez des environnements plus anciens.

Évaluer l'article

Moyenne: 0.0 / 5 · 0 évaluations

Tags

css has has() utilisation sélecteur css parent pseudo-classe :has() exemples comment utiliser :has() css :has() compatibilité navigateurs
Autor Xavier Moreau
Xavier Moreau
Je m'appelle Xavier Moreau et je cumule 14 ans d'expérience dans le développement web, avec un accent particulier sur JavaScript, le backend, le NoSQL et la sécurité. Mon intérêt pour ces domaines a émergé dès mes débuts dans la programmation, où j'ai découvert la puissance des technologies web et leur capacité à transformer des idées en réalité. J'aime expliquer des concepts complexes de manière accessible, en aidant les lecteurs à naviguer dans les défis techniques qu'ils rencontrent. Au fil des ans, j'ai développé une expertise solide en vérifiant mes sources, en comparant les informations et en simplifiant des sujets parfois ardus. Je m'efforce toujours de fournir des contenus utiles, précis et à jour, en suivant les tendances du secteur et en organisant mes connaissances de manière claire. Mon objectif est d'accompagner les passionnés et les professionnels du développement web dans leur quête de compréhension et d'innovation.

Commentaires (0)

Ajouter un commentaire