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.

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
- MDN – :has()
- WebKit blog – Using :has() as a CSS Parent Selector and much more
- Byungwoo Lee (Igalia) – CSS :has() a way of selecting parent
- CSSWG Selectors Level 4 – Relational Pseudo-class: :has()
- webstatus.dev – :has() Baseline és használati adatok
- Mozilla standards-positions – :has() pseudo class (position: positive)