Tous les articles
Sam. 4 avril 2026 · 5 min de lecture

🎨 CSS `:has()` et anchor positioning : ce que je n'écris plus en JS en 2026

CSS has et anchor positioning

📚 Introduction

Deux features CSS qui ont mis du temps à arriver, et qui sont désormais utilisables en production : le sélecteur :has() et le module CSS Anchor Positioning. Le premier permet de cibler un parent en fonction de ses enfants (le « sélecteur parent » que la communauté demandait depuis 20 ans). Le second permet de positionner un élément par rapport à un autre, sans JavaScript ni librairie de popper.

Ce post est un tour pratique : ce que je n’écris plus en JS depuis que ces deux features sont supportées, avec des exemples concrets que tu peux copier dans une feuille de style aujourd’hui.

🎯 :has() : le sélecteur parent natif

Disponibilité : Baseline Widely Available depuis décembre 2023 (Chrome 105+, Safari 15.4+, Firefox 121+). Tu peux l’utiliser partout en 2026.

L’idée : parent:has(child) matche le parent si une condition sur l’enfant est vraie.

/* Une card qui a un <img> reçoit un padding-top différent */
.card:has(img) {
	padding-top: 0;
}
 
/* Un form invalide colore son label */
.field:has(input:invalid) label {
	color: var(--color-error);
}
 
/* Un <li> qui contient une checkbox cochée se barre */
li:has(input[type='checkbox']:checked) {
	text-decoration: line-through;
	opacity: 0.5;
}

Concrètement, ça remplace une bonne partie des useEffect qui écoutaient un input pour appliquer une classe au parent. Le navigateur le fait, gratuitement, et sans cycle de rendu React.

Combinaisons utiles

:has() accepte n’importe quel sélecteur, y compris des combinateurs. Quelques patterns que j’utilise tout le temps :

/* Le body change de fond si une modal est ouverte */
body:has(dialog[open]) {
	overflow: hidden;
}
 
/* Le nav passe en mode compact si une sidebar est visible */
.app:has(.sidebar.is-open) .nav {
	width: 64px;
}
 
/* Un <details> ouvert se distingue */
details:has([open]) summary {
	border-bottom: 2px solid var(--accent);
}

Le navigateur gère le re-style en temps réel quand le DOM change. Pas besoin de MutationObserver.

📌 Anchor positioning : la fin des libs de tooltip

Disponibilité : Chrome/Edge 125+ (mai 2024), Safari 26+ (2025), Firefox 147+ (2025). Couverture globale ~83 % en 2026. Pour les navigateurs non-supportés, le fallback est généralement une position fixe acceptable.

L’idée : tu déclares un élément comme « ancre », puis tu positionnes d’autres éléments par rapport à elle, sans getBoundingClientRect().

<button id="trigger" class="anchor">Plus d'options</button>
<div class="popover">Menu déroulant...</div>
.anchor {
	anchor-name: --trigger;
}
 
.popover {
	position: fixed;
	position-anchor: --trigger;
	position-area: bottom span-right;
	margin-top: 8px;
}

anchor-name déclare l’ancre, position-anchor attache un élément. position-area est une grille 3×3 autour de l’ancre : top, bottom, left, right, center, et leurs combinaisons. Très intuitif quand tu connais les utilitaires Tailwind.

Position calculée via anchor()

Pour un contrôle plus fin :

.popover {
	position: fixed;
	position-anchor: --trigger;
	top: anchor(bottom);
	left: anchor(center);
	translate: -50% 0;
}

anchor(bottom) renvoie la coordonnée du bord bas de l’ancre. Tu combines avec translate et calc() pour le positionnement précis.

Taille calquée sur l’ancre

anchor-size() permet de copier la taille de l’ancre :

.popover {
	width: anchor-size(width);
	min-width: 200px;
}

Très utile pour les comboboxes / dropdowns où le menu doit faire la largeur du bouton.

Fallback positions

Le navigateur peut basculer la position si la popover déborde du viewport. Tu déclares un nom de stratégie et tu lui passes des positions de secours :

.popover {
	position-anchor: --trigger;
	position-area: bottom;
	position-try-fallbacks: top, right, left;
}

Si bottom déborde, le navigateur essaie top, puis right, puis left. C’est exactement ce que faisaient Popper.js et Floating UI à coups de calculs JavaScript. Plus besoin d’embarquer 8 Ko de lib.

🧩 Patterns concrets remplacés

Tooltip pur CSS

<button class="tooltip-anchor">Hover me</button>
<div class="tooltip">Plus d'infos</div>
.tooltip-anchor {
	anchor-name: --tip;
}
 
.tooltip {
	position: fixed;
	position-anchor: --tip;
	position-area: top;
	opacity: 0;
	transition: opacity 0.15s;
}
 
.tooltip-anchor:hover ~ .tooltip,
.tooltip-anchor:focus-visible ~ .tooltip {
	opacity: 1;
}

Filtres conditionnels (:has())

/* Cache "Aucun résultat" sauf si aucune card n'est visible */
.results:has(.card) .empty-state {
	display: none;
}
 
.results:not(:has(.card)) .empty-state {
	display: block;
}

Accordion qui désactive son voisin

/* Si un <details> est ouvert, les autres se grisent */
.faq:has(details[open]) details:not([open]) {
	opacity: 0.5;
}

Le tout sans une ligne de JavaScript.

⚠️ Quelques précautions

  • Polyfills : il existe des polyfills pour anchor-positioning (oddbird/css-anchor-positioning), mais le bundle est lourd (~20 Ko). Pèse l’option : tu peux souvent te contenter d’un fallback dégradé en position statique pour les vieux navigateurs.
  • Spécificité :has() : le sélecteur a la spécificité de son argument le plus fort, ce qui peut surprendre. Lis la spec MDN en cas de cascade qui foire.
  • Performance :has() : c’est calculé à chaque mutation du DOM. Sur des arbres très gros (10 000+ nœuds), profile avant de t’enflammer.
  • Accessibilité : le positionnement CSS ne suffit pas pour un menu déroulant accessible. Tu as encore besoin d’aria-expanded, aria-controls, gestion clavier. Pense au popover attribute HTML natif qui complète très bien anchor-positioning.

🎯 Combo avec popover natif

L’attribut HTML popover (Baseline 2024) est le compagnon idéal d’anchor positioning. Tu obtiens un menu déroulant accessible, focus-trap géré, ESC pour fermer, sans JavaScript :

<button popovertarget="menu" id="trigger" class="anchor">Menu</button>
<div popover id="menu" class="popover">
	<button>Action 1</button>
	<button>Action 2</button>
</div>
.anchor {
	anchor-name: --t;
}
 
[popover] {
	position-anchor: --t;
	position-area: bottom span-right;
	margin-top: 8px;
	position-try-fallbacks: top span-right;
}

C’est la plus belle combinaison de specs CSS récentes pour faire propre, accessible, et performant en 2026.

🎉 Conclusion

:has() et anchor positioning sont les deux meilleures additions CSS de la décennie pour les développeurs front. Combinés au popover natif, ils rendent inutile la majorité des micro-libs JavaScript qu’on traînait pour faire des tooltips, dropdowns et états conditionnels.

Pour les projets neufs en 2026, j’écris directement en CSS natif et je tombe sur un polyfill seulement si l’analytics du client le justifie. Si tu maintiens un Astro / Vue / React, regarde où tu peux jeter quelques useEffect qui ne servaient qu’à styler un parent. Tu vas être surpris.

🔗 Liens utiles

/faq

Questions fréquentes

À quoi sert le sélecteur CSS :has() ?

+

Le sélecteur :has() permet de cibler un parent en fonction de ses enfants : « parent:has(child) » matche le parent si une condition sur l'enfant est vraie. C'est le « sélecteur parent » natif, Baseline Widely Available depuis décembre 2023.

Qu'apporte l'anchor positioning en CSS ?

+

L'anchor positioning permet de positionner un élément par rapport à un autre déclaré comme ancre, sans JavaScript ni getBoundingClientRect(). On utilise anchor-name, position-anchor, position-area, ainsi que anchor() et anchor-size() pour un contrôle plus fin.

Peut-on remplacer Popper.js et Floating UI par du CSS pur ?

+

Oui : avec position-try-fallbacks, le navigateur bascule la position de la popover si elle déborde du viewport, ce que faisaient ces libs à coups de calculs JavaScript. Pour un menu accessible, on garde toutefois aria-expanded, aria-controls et la gestion clavier.