CSS :has() — vanhempi, joka tietää mitä sisällä tapahtuu
CSS :has() — vanhempi, joka tietää mitä sisällä tapahtuu
Sinulla on lomakekenttä: <div class="field">, jonka sisällä on selite ja input. Vaatimus kuulostaa triviaalilta – kun input on virheellinen, koko säiliön pitäisi saada punainen reunaviiva ja varoituskuvake selitteen viereen, ei vain itse inputin. Tartut selektoriin .field input:invalid – ja törmäät seinään. Tämä selektori tyylittäisi inputin, jos se haluaisi punaisen reunaviivan. Mutta sinä haluat tyylittää .field-elementin, eli tämän inputin vanhemman, sen perusteella, mitä sen sisällä tapahtuu. Ja yllättäen käy ilmi, että CSS – kymmenistä kombinaattoreista, pseudoluokista ja attribuuttiselektoreista huolimatta – ei koko 25-vuotisen historiansa aikana ole koskaan tarjonnut tapaa tehdä tätä.
Tämä ei ole aukko CSS-osaamisessasi. Tämä on selektoriarkkitehtuurin perustavanlaatuinen ominaisuus, joka oli voimassa CSS1:stä vuoteen 2022 asti.
Miksi kombinaattori katsoo aina vain yhteen suuntaan
Jokainen CSS:n kombinaattori – välilyönti (jälkeläinen), > (suora lapsi), + (vieressä oleva sisarus), ~ (yleinen sisarus) – kuvaa suhteen kahden selektorin välillä, mutta tyylitettävä elementti on aina kombinaattorin oikealla puolella oleva. .field input tyylittää .field-elementin sisällä olevan inputin. .field ~ .error tyylittää .error-elementin, joka on .field:n sisarus. Suunta on aina sama: kontekstista kohteeseen, ei koskaan toisin päin. CSS-moottorilla ei ole .field-elementtiä kohdatessaan sisäänrakennettua mekanismia "kurkistaa sisään" ja muuttaa sen perusteella päätöstä itse .field-elementin tyylistä.
Kehittäjät kiersivät tämän vuosien ajan kahdella tavalla, molemmilla todellisin kustannuksin. Ensimmäinen – JavaScript, joka kuuntelee input/blur-tapahtumaa ja lisää käsin .field--invalid-luokan vanhemmalle. Toinen, ovelampi mutta hauraampi – niin kutsuttu checkbox/radio-hakkerointi, joka käyttää ~-kombinaattoria sisaruksen tyylittämiseen :checked-tilan perusteella, mikä toimii vain silloin, kun elementit ovat todella sisaruksia litteässä DOM-rakenteessa, eivätkä sisäkkäisiä, kuten todellinen lomake vaatii.
jsx
// Kiertotie ennen :has()-ominaisuutta – toimii, mutta vaatii JS:ää johonkin,// mikä on puhtaasti HTML-tilan visuaalinen seurausimport { useState } from "react";const FormField = ({ label, ...inputProps }) => {const [isInvalid, setIsInvalid] = useState(false);return (<div className={`field ${isInvalid ? "field--invalid" : ""}`}><label>{label}</label><input{...inputProps}onBlur={(e) => setIsInvalid(!e.target.validity.valid)}/></div>);};
:has() eliminoi tämän koodin, ei siksi että se olisi ovelampi temppu – vaan siksi että se ratkaisee ongelman juuresta, CSS:n historian ensimmäisenä relaationaalisena pseudoluokkana, joka antaa elementin kysellä omaa sisäistään.
Miten :has() oikeasti toimii: elementtiin ankkuroitu selektori, ei jälkeläiseen
Ratkaiseva ajattelun muutos: .field:has(input:invalid) ei tyylitä inputia. Se tyylittää .field-elementin – juuri sen elementin, johon pseudoluokka on kiinnitetty – sillä ehdolla, että sen sisällä jossain on elementti, joka täsmää suluissa olevaan selektoriin. Elementti, johon "ankkuroidut" (engl. anchor element), pysyy aina samana; :has() vain päättää, saako se ylipäätään osuman.
css
.field:has(input:invalid) {border-color: #c62828;}.field:has(input:invalid) .field__icon {visibility: visible;}
Oletuksena :has():n sisällä oleva selektori etsii mitä tahansa jälkeläistä, millä tahansa syvyydellä – aivan kuten tavallinen välilyönti. Mutta voit rajata suhdetta täsmälleen samalla tavalla kuin tavallisessa CSS:ssä, käyttämällä kombinaattoreita suoraan sulkujen sisällä:
css
/* Vain suora lapsi, ei mikä tahansa jälkeläinen */.card:has(> img) {grid-template-columns: 120px 1fr;}/* Elementti, jota VÄLITTÖMÄSTI seuraa .error-message sisaruksena */.field:has(+ .field-hint) {margin-bottom: 4px;}
Tämä tekee :has():sta ei vain yhden uuden tempun, vaan yleistyksen – se antaa ilmaista CSS:ssä minkä tahansa suhteen, joka aiemmin oli kuvattavissa kombinaattorilla, mutta suunnattuna "sisäänpäin" tai "taaksepäin" pelkän "eteenpäin"-suunnan sijaan.
Käytännön esimerkki: lomakekenttä, joka tietää itse olevansa virheellinen
Yhdistetään tämä täydelliseksi, saavutettavaksi komponentiksi. Tärkeä yksityiskohta: :invalid itsessään käynnistyy heti, kun tyhjä required-attribuutilla varustettu kenttä latautuu – ennen kuin käyttäjä on ehtinyt kirjoittaa mitään, mikä antaisi punaisen reunaviivan tyhjälle, koskemattomalle lomakkeelle. Tämän ratkaisee :not(:placeholder-shown), joka täsmää kenttään vain silloin, kun käyttäjä on todella jo jättänyt siihen jotain.
jsx
const FormField = ({ label, id, ...inputProps }) => (<div className="field"><label htmlFor={id}>{label}</label><input id={id} {...inputProps} /><svg className="field__icon" aria-hidden="true" viewBox="0 0 20 20"><path d="M10 2a8 8 0 100 16 8 8 0 000-16zm1 12H9v-2h2v2zm0-4H9V5h2v5z" /></svg></div>);export default FormField;
css
.field {position: relative;border: 1px solid #ccc;border-radius: 8px;padding: 8px 12px;transition: border-color 0.15s ease;}.field__icon {position: absolute;right: 12px;top: 50%;transform: translateY(-50%);width: 18px;fill: #c62828;visibility: hidden;}/* Kenttä on "kosketettu" (jotain kirjoitettu) JA virheellinen – vasta silloin reagoi */.field:has(input:not(:placeholder-shown):invalid) {border-color: #c62828;background: #fff5f5;}.field:has(input:not(:placeholder-shown):invalid) .field__icon {visibility: visible;}/* Positiivinen vahvistus on yhtä tärkeä kuin virhe */.field:has(input:not(:placeholder-shown):valid) {border-color: #2e7d32;}
Ei ainuttakaan riviä JavaScriptia ulkoasusta vastaamassa. Validointitila on jo olemassa natiivisti selaimessa – :valid/:invalid ovat olleet siellä CSS3:sta lähtien – :has() vain viimein antoi siirtää tämän olemassa olevan tilan inputista sen säiliöön, sinne missä sitä on visuaalisesti tarvittu aina.
Toinen kaava: komponentti, joka tietää mitä se sisältää
Sama mekanismi ratkaisee täysin toisenlaisen ongelmaluokan – asettelun, joka riippuu tietyn sisällön olemassaolosta, ilman propsia, joka kertoisi siitä etukäteen.
css
/* Kuvallinen kortti saa kaksipalstaisen asettelun, kuvaton kortti – täysleveän tekstin */.card:has(> img) {display: grid;grid-template-columns: 96px 1fr;gap: 12px;}/* Osion otsikko saa alamarginaalin vain, jos osiolla on todella alaotsikko */.section:has(> .section__subtitle) .section__title {margin-bottom: 4px;}/* Lista ilman yhtäkään elementtiä näyttää tyhjän tilan pelkän tyhjän tilan sijaan */.product-list:not(:has(li)) {display: block;}.product-list:not(:has(li))::after {content: "Ei kriteerit täyttäviä tuotteita.";color: #666;}
Ilman :has():ää jokainen näistä tapauksista vaatisi joko ylemmän komponentin käsin asettaman propsin (hasImage, isEmpty) tai taulukon pituuden tarkistamisen JSX:ssä ja erillisen luokan ehdollisen renderöinnin. :has() antaa komponentin itse tunnistaa oman sisältönsä ja reagoida siihen, sen sijaan että siitä kerrottaisiin ulkopuolelta – tämä on täsmälleen sama ajattelusuunta kuin Container Queries -artikkelin omasta säiliöstään tietoinen tuotekortti: vähemmän käsin välitettyä tilaa, enemmän suoraan rakenteesta seuraavaa logiikkaa.
Sudenkuopat ja hyvät käytännöt
:has():n spesifisyys on sen sisällä olevan spesifisimmän selektorin spesifisyys, ei nolla..field:has(input:invalid):n spesifisyys on kahden luokan ja pseudoluokan yhteissumma – älä luota siihen, että:has()"ei laske" spesifisyyteen, koska käytännössä se ohittaa säännöllisesti yksinkertaisemmat, myöhemmin arkissa kirjoitetut säännöt.- Syvästi sisäkkäinen, laaja
:has()ilman kombinaattoria voi maksaa..app:has(.some-deeply-nested-element)käskee teoriassa moottoria harkitsemaan koko.app:n alipuuta joka kerta, kun sen sisällä tapahtuu DOM-muutos. Nykyaikaiset moottorit (Chromium, WebKit) optimoivat tätä niin kutsutuilla invalidation seteillä – ne eivät laske kaikkea sokkona uudelleen – mutta vaikutus riippuu tarkasta selektorista ja puun koosta. Rajaa suhdetta kombinaattorilla (>, suora lapsi) aina kun mahdollista, oletusarvoisen syvyyshaun sijaan. :has()ei korvaa:focus-within:ää. Jos tarvitset vain "vanhempi reagoi, kun jälkeläisellä on fokus",:focus-withinon ollut olemassa jo pitkään, se on halvempi laskea ja luettavampi –:has(:focus)antaa käytännössä saman lopputuloksen, mutta ilman syytä tarttua yleisempään työkaluun siellä, missä erikoistunut jo on olemassa.- Selaintuki lakkasi olemasta ongelma.
:has()saapui palapelin viimeisenä suurena palana – Safarissa se on ollut versiosta 15.4 (maaliskuu 2022), Chromessa versiosta 105 (elokuu 2022), Firefox liittyi mukaan viimeisenä, versiossa 121 (joulukuu 2023). Siitä lähtien sitä on turvallista käyttää ilman@supports-sääntöä jokaisessa uudessa projektissa.
Yhteenveto
25 vuoden ajan CSS salli kuvata vain yhteen suuntaan kulkevia suhteita – kontekstista kohteeseen, vanhemmasta lapseen, edeltäjästä seuraajaan. :has() ei ole saman idean uusi muunnelma, vaan ensimmäinen pseudoluokka, joka antaa elementin kysellä omaa sisäistään ja reagoida siihen, mitä sieltä löytyy – validointivirhe kentässä, kuvan olemassaolo kortissa, elementtien puuttuminen listalta. Tämä siirtää kokonaisen päätösluokan, joka aiemmin joutui menemään JavaScriptiin tai käsin ylhäältä välitettyihin propseihin, takaisin sinne minne se kuuluu: HTML-rakenteeseen ja CSS-sääntöihin, jotka tuota rakennetta kuvaavat.