Intégration web angularforall.com

- CSS :has() : le sélecteur parent enfin natif

Css-Has Selecteur-Parent Css-Moderne Selecteurs-Css Forms-Validation Integration-Web Front-End Css3 Html5 Accessibilite Design-System Bootstrap-5 Ui-Conditionnel Progressive-Enhancement
CSS :has() : le sélecteur parent enfin natif

Maîtrisez le sélecteur :has() en CSS : syntaxe, validation forms sans JS, layouts conditionnels, performances et fallback pour navigateurs anciens.

Pourquoi :has() change tout

Pendant 20 ans, CSS n'a jamais permis de styler un élément en fonction de ses enfants. Le rêve d'un sélecteur parent — li < a dans les anciennes spécifications jamais implémentées — restait inaccessible. Pour styler un <article> contenant une image, il fallait ajouter une classe via JavaScript : document.querySelector('article').classList.add('has-image'). Le code se multipliait, le DOM clignotait au chargement, et la maintenabilité s'effondrait dès que la structure HTML évoluait.

Depuis fin 2023, tous les navigateurs majeurs supportent nativement :has(). Cette pseudo-classe fonctionnelle prend un sélecteur en argument et teste la présence d'éléments correspondants dans le sous-arbre du candidat. C'est, en pratique, le sélecteur parent que tout intégrateur attendait.

Cas d'usage clé en une ligne

/* Styler une card qui contient une image */
.card:has(img) { padding-top: 0; }

/* Styler un label dont l'input est invalide */
label:has(input:invalid) { color: red; }

/* Styler le body si une modale est ouverte */
body:has(dialog[open]) { overflow: hidden; }

/* Styler un parent en fonction d'un état enfant — sans JS */
.product:has(.badge-promo) { border: 2px solid orange; }
Compatibilité 2026 : Chrome/Edge 105+ (sept. 2022), Safari 15.4+ (mars 2022), Firefox 121+ (déc. 2023). Plus de 92% des utilisateurs mondiaux supportent :has() sans polyfill — c'est désormais une feature de production sûre.

Syntaxe et fonctionnement

La syntaxe est simple : cible:has(condition). La cible est l'élément qui sera stylé. La condition est un sélecteur relatif évalué dans le sous-arbre de la cible.

Anatomie d'un sélecteur :has()

/* Cible : article — Condition : possède au moins un h2 */
article:has(h2) {
  margin-top: 2rem;
}

/* Cible : section — Condition : possède un .alert.alert-danger en descendance */
section:has(.alert.alert-danger) {
  background: #fff5f5;
}

/* Cible : form — Condition : a un input requis vide */
form:has(input:required:placeholder-shown) button[type="submit"] {
  opacity: 0.5;
  pointer-events: none;
}

Combinateurs relatifs : descendance, frère, enfant direct

/* Descendance directe ou indirecte (par défaut) */
.menu:has(.active) { /* tout .active dans .menu */ }

/* Enfant direct uniquement avec > */
.menu:has(> .active) { /* .active uniquement enfant direct */ }

/* Frère adjacent avec + */
h2:has(+ p) { /* h2 immédiatement suivi d'un p */ }

/* Frère général avec ~ */
h2:has(~ figure) { /* h2 suivi quelque part par une figure */ }

Combinaisons multiples

/* Possède une image ET un titre */
.card:has(img):has(h3) { /* style enrichi */ }

/* Possède une image OU une vidéo */
.media:has(img, video) { /* style générique média */ }

/* Possède un .error mais PAS de .resolved */
.alert:has(.error):not(:has(.resolved)) {
  background: #fee;
  border-left: 4px solid red;
}
À l'intérieur de :has(), les sélecteurs sont relatifs à la cible. Le sélecteur img dans .card:has(img) signifie « une image quelque part dans .card », pas « une image globale ».

Forms : valider visuellement sans JS

Le cas d'usage le plus impactant de :has() est la validation visuelle des formulaires. Avant, chaque champ invalide demandait un listener JavaScript pour ajouter une classe sur son <label> ou son <fieldset>. Maintenant, tout est déclaratif.

Label en rouge si l'input est invalide

<label class="form-label">
  Email
  <input type="email" required class="form-control">
</label>
/* Label en rouge si son input est invalide ET a déjà été touché */
.form-label:has(input:invalid:not(:placeholder-shown)) {
  color: #dc3545;
}

/* Label en vert si valide */
.form-label:has(input:valid:not(:placeholder-shown)) {
  color: #198754;
}

Fieldset entier signalé si un champ a une erreur

<fieldset class="border p-3 rounded">
  <legend class="float-none w-auto px-2">Adresse</legend>
  <input type="text" name="rue" required class="form-control mb-2">
  <input type="text" name="ville" required class="form-control mb-2">
  <input type="text" name="cp" pattern="\d{5}" required class="form-control">
</fieldset>
/* Bordure rouge sur tout le fieldset si au moins un champ est invalide */
fieldset:has(input:invalid:not(:placeholder-shown)) {
  border-color: #dc3545 !important;
  box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.15);
}

/* Légende en rouge en cohérence */
fieldset:has(input:invalid:not(:placeholder-shown)) legend {
  color: #dc3545;
  font-weight: 700;
}

Bouton submit désactivé si formulaire incomplet

/* Désactivation visuelle automatique */
form:has(input:required:placeholder-shown, input:invalid:not(:placeholder-shown)) button[type="submit"] {
  opacity: 0.45;
  cursor: not-allowed;
  pointer-events: none;
}

/* Activation explicite quand tout est OK */
form:not(:has(input:invalid)):has(input:required:not(:placeholder-shown)) button[type="submit"] {
  background: linear-gradient(90deg, #22c55e, #16a34a);
  color: white;
}
UX optimale : Le sélecteur :placeholder-shown détecte un champ resté vide. Combiné à :invalid, vous évitez de signaler une erreur tant que l'utilisateur n'a pas commencé à taper — une bonne pratique d'accessibilité.

Layouts conditionnels

:has() permet aussi d'adapter le layout d'un parent en fonction de la présence ou de l'absence d'éléments. Plus besoin de classes has-sidebar, no-image, variant-empty ajoutées en JavaScript ou Twig.

Layout adaptatif selon la présence d'une sidebar

<div class="layout">
  <main>Contenu principal</main>
  <aside>Sidebar optionnelle</aside>
</div>
.layout {
  display: grid;
  gap: 2rem;
}

/* Si pas de sidebar : contenu pleine largeur */
.layout:not(:has(aside)) {
  grid-template-columns: 1fr;
}

/* Avec sidebar : 2 colonnes */
.layout:has(aside) {
  grid-template-columns: 1fr 320px;
}

Card qui s'adapte à son contenu

.card:has(img:first-child) { padding-top: 0; }
.card:has(.badge-featured) { border: 3px solid gold; }
.card:has(video) { aspect-ratio: 16 / 9; }
.card:not(:has(p, .description)) { padding-block: 2rem; text-align: center; }

Galerie qui dégrade le grid si peu d'éléments

/* Galerie classique en 4 colonnes */
.gallery {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 1rem;
}

/* Si la galerie ne contient qu'un ou deux items, centrer */
.gallery:has(:nth-child(2):last-child),
.gallery:has(:only-child) {
  grid-template-columns: minmax(auto, 600px);
  justify-content: center;
}

Body qui bloque le scroll quand modal/dialog ouverte

/* Plus besoin d'ajouter une classe .modal-open via JS Bootstrap */
body:has(dialog[open]),
body:has(.modal.show) {
  overflow: hidden;
  padding-right: var(--scrollbar-width, 0px);
}

Combinaisons avec :is(), :not(), :where()

:has() se combine élégamment avec les autres pseudo-classes fonctionnelles modernes pour produire des sélecteurs concis et puissants.

:has() + :not() — exclure des cas

/* Article qui contient un titre, mais pas de figure */
article:has(h2):not(:has(figure)) {
  max-width: 720px;
}

/* Card sans aucun bouton */
.card:not(:has(button, .btn, [role="button"])) {
  padding-bottom: 1.5rem;
}

:has() + :is() — multi-cibles

/* Tout titre qui suit immédiatement une figure */
:is(h2, h3, h4):has(figure + &) { /* note : & non standard ici */ }

/* Plus correct avec :is() en cible */
figure + :is(h2, h3, h4) { margin-top: 0.25em; }

/* :has() applique :is() en interne */
section:has(:is(h2, h3) + p) { /* sections où un titre est immédiatement suivi d'un paragraphe */ }

:has() + :where() — spécificité zéro

/* :where() retourne une spécificité de 0,0,0 */
/* Permet de surcharger facilement plus tard */
article:where(:has(.featured)) {
  background: #fffbe5;
}

/* Override simple sans !important */
article.no-highlight {
  background: transparent;
}
La spécificité de :has() est celle du sélecteur le plus spécifique dans son argument. div:has(.foo) a la spécificité d'une classe (0,1,0) plus celle du div (0,0,1) = (0,1,1).

Performance et bonnes pratiques

Au début, :has() faisait peur : on craignait des recalculs massifs à chaque changement de DOM. En 2026, les moteurs CSS (Blink, WebKit, Gecko) ont stabilisé les optimisations. Les benchmarks montrent un coût négligeable sur des arbres standards (< 5000 éléments par cible).

Règles d'or de performance

Optimisations à appliquer :
  • ✅ Préférer .cible:has(.classe) à :has(.classe) seul (cible plus restreinte)
  • ✅ Limiter la portée : article:has(> img) est plus rapide que article:has(img)
  • ✅ Éviter les :has() imbriqués sur 3+ niveaux
  • ✅ Combiner avec des classes sémantiques quand le pattern se répète
  • ❌ Ne pas utiliser :has() dans des animations sur composants à fort taux de mutation

Mesurer en pratique

/* À éviter dans une grille de 10 000 lignes — coût élevé */
table tr:has(td.error) { background: #fee; }

/* Préférer une portée explicite ou ajouter une classe */
table.with-validation tbody tr:has(td.error) { background: #fee; }

/* Mieux : ajouter la classe côté serveur ou via JS au render */
table tr.row-error { background: #fee; }

Fallback et progressive enhancement

Pour les 8% restants (anciens Firefox < 121, navigateurs legacy d'entreprise), utilisez la requête fonctionnelle @supports selector().

Détection moderne avec @supports selector()

/* Style de base pour tous */
.card {
  border: 1px solid #ddd;
  padding: 1rem;
}

/* Amélioration progressive si :has() est supporté */
@supports selector(:has(*)) {
  .card:has(img) {
    padding-top: 0;
  }
  .card:has(.badge-promo) {
    border-color: orange;
    border-width: 2px;
  }
}

/* Fallback explicite avec classe (gérée par JS) */
@supports not selector(:has(*)) {
  .card.has-image { padding-top: 0; }
  .card.has-promo { border-color: orange; border-width: 2px; }
}

JavaScript de fallback minimal

// Fallback léger : ajoute des classes côté legacy
if (!CSS.supports('selector(:has(*))')) {
  document.querySelectorAll('.card').forEach(card => {
    if (card.querySelector('img')) card.classList.add('has-image');
    if (card.querySelector('.badge-promo')) card.classList.add('has-promo');
  });
}

Cas réels : 8 exemples production

1. Navbar qui change si dropdown ouvert

.navbar:has(.dropdown.show) {
  box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}

2. Carte produit en rupture

.product:has([data-stock="0"]) {
  filter: grayscale(0.6);
  position: relative;
}
.product:has([data-stock="0"])::after {
  content: 'Rupture';
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  background: rgba(255,255,255,0.7);
  font-weight: 700;
}

3. Liste à puces sans puces si elle ne contient qu'un seul item

ul:has(li:only-child) {
  list-style: none;
  padding-left: 0;
}

4. Article qui décale son sommaire si une figure le précède

.article-toc:has(+ .article-section figure:first-child) {
  margin-bottom: 3rem;
}

5. Form Bootstrap 5 — input-group rouge si invalide

.input-group:has(.form-control:invalid:not(:placeholder-shown)) {
  outline: 2px solid #dc3545;
  outline-offset: -1px;
  border-radius: 0.375rem;
}

6. Card qui agrandit son padding si peu de contenu

.card:has(.card-body:only-child:not(:has(p))) {
  padding: 3rem;
  text-align: center;
}

7. Sidebar qui se replie si vide

aside:not(:has(*)) {
  display: none;
}
.layout:not(:has(aside *)) main {
  grid-column: 1 / -1;
}

8. Mode focus complet sur formulaire actif

/* Si un input du form est focus, atténuer le reste de la page */
body:has(form input:focus) main > *:not(:has(input:focus)) {
  opacity: 0.6;
  transition: opacity 0.3s ease;
}
Astuce DX : Sauvegardez une bibliothèque interne de patterns :has() en commentaires de votre main.css. Cela évite de réinventer la même logique sur chaque projet.

Conclusion

Le sélecteur :has() est l'un des ajouts CSS les plus impactants de la décennie. Il supprime un pan entier de JavaScript dédié à la manipulation de classes pour styler des parents, simplifie radicalement la validation visuelle des formulaires, et rend les layouts conditionnels purement déclaratifs. Avec 92%+ de couverture mondiale en 2026, il est désormais une feature de production sûre.

Adoptez :has() par étape : commencez par les patterns à fort retour (validation forms, cards conditionnelles), couvrez le legacy via @supports selector(:has(*)), et documentez les patterns récurrents dans votre design system. Vos templates HTML deviennent plus simples, votre JavaScript plus mince, et votre CSS exprime enfin l'intention complète sans béquille.

Récapitulatif :
  • :has(condition) stylise un parent en fonction de ses descendants ou voisins
  • Combinateurs internes : descendance, >, +, ~
  • Combine parfaitement avec :not(), :is(), :where()
  • Validation forms sans JS : label:has(input:invalid)
  • Layouts adaptatifs : .layout:has(aside)
  • Body lock : body:has(dialog[open])
  • Fallback avec @supports selector(:has(*))
  • Restreindre la cible pour la performance

Partager