A :has(): a szülő-szelektor, amire két évtizedig várt a CSS

A CSS sosem tudott reagálni arra, mi van egy elem belsejében – ezért kellett mindig JavaScript. A has() ezt megoldja, de nem trükk: azért tartott ilyen sokáig, mert szétveri a böngészők érvénytelenítési logikájának alapfeltevését.

Ha valaha írtál olyan JavaScript-kódot, amelynek egyetlen dolga volt egy MutationObserver-t rátenni egy konténerre, hogy amint megjelenik benne egy kép, felrakjon egy classt a szülőre – gratulálok, kézzel újraimplementáltad azt, amit a CSS mostanra egy sornyi szelektorral old meg. Csak közben valószínűleg nem gondoltál bele, hogy ez a probléma a böngészőgyártóknak közel két évtizedig tényleg megoldhatatlannak tűnt. Nem lustaságból, hanem mert szétveri a stílus-érvénytelenítés teljes alapfeltevését.

A pszeudo-osztály neve :has(), és ha eddig kihagytad, itt az idő: mind a négy nagy motor (Chromium, WebKit, Gecko) támogatja, a Baseline „widely available” státuszt idén júniusban érte el. És ami tényleg meglepő: a webstatus.dev által közölt Chrome-telemetria szerint a böngészőben megnyitott lapok kb. 51%-ában legalább egy :has()-t tartalmazó szabály lefut. Ez majdnem annyi, mint a flexbox aránya (84%) – vagyis ez már rég nem egy niche trükk, hanem alapinfrastruktúra, még ha ennek nagy része valószínűleg design system-ekből és keretrendszerekből szivárog be, nem kézzel írt szelektorokból.

Diagram: felül a hagyományos CSS-szelektorok esetén a stílus-érvénytelenítés csak lefelé, ősből gyerek felé terjed, a felfelé irányuló próbálkozás áthúzva jelzi, hogy ez nem működik; alul a :has() esetén több, mélyen lévő gyerek elem változása felfelé, az ős elemig érvényteleníti a stílust, amit egy zölden világító pipa jelöl.

Mit nem tudott eddig a CSS

A CSS combinatorai (szóköz, >, +, ~) mindig csak egy irányba néznek: leszármazottra, testvérre, rákövetkező elemre. Arra sosem volt módod, hogy egy elem stílusát a tartalma alapján döntsd el. „Ha van benne kép, csinálj mást” – ez évtizedek óta a klasszikus „CSS parent selector” panasz, amit rendszeresen kikövetelt a közösség a CSS Working Grouptól.

Ma ez így néz ki:

.card:has(img) {
  grid-template-rows: auto 1fr;
}

.card:not(:has(img)) {
  grid-template-rows: 1fr;
}

Vagy a klasszikus form-validáció, JS és aria-invalid-figyelő callback nélkül:

.field:has(:invalid) {
  border-color: #e5484d;
}

Vagy egy sokszor emlegetett testvér-eset az MDN dokumentációjából: egy h1, amelyet közvetlenül egy h2 követ, kisebb margót kap alul – tisztán tipográfiai döntés, amit korábban csak úgy lehetett megoldani, hogy JS-ből számoltad ki, mi következik utána.

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

Miért tartott ez húsz évet

Itt jön a rész, amit a legtöbb cikk kihagy, és pont ez a lényeg. A böngészők stílus-motorja arra van optimalizálva, hogy egy DOM-mutáció után csak lefelé kelljen újraszámolni: ha megváltozik egy elem, az invalidáció a leszármazottai felé terjed, mert a hagyományos szelektorok (leszármazott, testvér, attribútum) mind ebbe az irányba mutatnak. Ez az alapfeltevés van bedrótozva minden nagyobb rendering motorba.

A :has() pontosan ezt fordítja meg. A .subject:has(.mutation) azt jelenti, hogy egy mélyen lévő gyerek megváltozása az ősét kell újra kiértékelje – felfelé, nem lefelé. Byungwoo Lee (Igalia), aki a Chromiumban vezette a :has() implementációját, pontosan ezt írja le a blogján: a meglévő invalidációs logika arra épül, hogy egy mutáció hatása „az elem maga, vagy tőle lefelé” keresendő, és a :has() esetében ez a keresési tartomány egy leszármazott-fáról hirtelen az egész fára tágulhat ki – ami vagy komoly teljesítményromlást okoz, vagy egyszerűen nem illeszthető be a régi architektúrába.

A WebKit csapata sem finomkodott ezzel: a saját blogbejegyzésükben úgy fogalmaznak, hogy sokáig „lehetetlennek tűnt kitalálni, hogyan tudna egy böngészőmotor kezelni egy potenciálisan nagyon komplex, körkörös mintát, és elég gyorsan végigszámolni rajta”. Ezért kellett mindhárom motornak külön, :has()-specifikus gyorsítótárazási és szűrési technikát kifejlesztenie – ez nem ugyanaz a kód, ami a sima szelektorokat gyorsítja, hanem egy teljesen új réteg.

Ez az a mondat, amit érdemes megjegyezni: nem azért nem volt eddig szülő-szelektor a CSS-ben, mert senki nem akarta – azért, mert az invalidáció iránya technikailag az ellenkezője annak, amire a motorok épültek.

A csapdák, amikről a demók nem beszélnek

A :has() nem varázslat, és van ára.

  • Specificitás: a :has() a benne lévő legspecifikusabb szelektor specificitását veszi fel, pont úgy, mint az :is() és a :not(). Egy :has(#id) ugyanannyit nyom a latban, mint egy explicit ID-szelektor.
  • Ne horgonyozd le túl szélesre. body:has(...), :root:has(...) vagy *:has(...) azt jelenti, hogy bármilyen DOM-változás a teljes dokumentumban kiváltja a :has() kiértékelését. Az MDN egyenesen kerülendőnek nevezi ezt – a belső szelektorban pedig érdemes > vagy + kombinátort használni, hogy a motor ne kelljen bejárja a teljes altfát minden mutációnál.
  • Pszeudo-elemek nem engedettek se a :has() argumentumában, se horgonyként – ez direkt CSSWG-döntés, mert sok pszeudo-elem (pl. ::first-line) csak az ős stílusa alapján létezik, ami körkörös függést okozna.
  • Nem ágyazható tetszőlegesen egymásba: a :has() nem tartalmazhat saját magát.

Használnád production-ben?

Igen, de célzottan. Komponens-szintű, „szűk hatókörű” döntésekre – van-e kép a kártyában, van-e checked input a szülő alatt, van-e hibás mező az űrlapban – ez pontosan az a feladat, amit korábban felesleges JS-sel oldottunk meg, és most egy sornyi CSS-sel gyorsabb és karbantarthatóbb. A dark mode toggle :has(input:checked)-del, checkbox hack nélkül, ma már tankönyvi példa.

Amit nem tennék: nagy, gyakran mutáló DOM-fákon (élő dashboard, chat, infinite scroll lista) :has()-t horgonyozni egy magas szintű konténerre. Ott pont azt a problémát hozod vissza, amit a böngészőknek éveken át kellett megoldaniuk – csak most a te renderelési költségedként jelenik meg. Ha Firefox ESR-t vagy nagyon régi Safarit (15.4 előtt) is támogatnod kell, @supports selector(:has(a))-val van visszaesési út, de 2023 decembere (Firefox 121) óta ez egyre inkább csak papíron probléma.

Források

Leave a Reply

Az e-mail címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük