CSS :has() — forelderen som vet hva som skjer inni
Du har et skjemafelt: <div class="field"> med en etikett og en input inni. Kravet høres banalt ut – når input-en er ugyldig, skal hele beholderen få en rød ramme og et advarselsikon ved siden av etiketten, ikke bare selve input-en. Du griper til .field input:invalid – og treffer en vegg. Denne selektoren ville stylet input-en, hvis den ønsket en rød ramme. Men du vil style .field, altså forelderen til den input-en, basert på det som skjer inni den. Og plutselig viser det seg at CSS – til tross for dusinvis av kombinatorer, pseudoklasser og attributtselektorer – aldri i sin 25 år lange historie har gitt en måte å gjøre dette på.
Dette er ikke et hull i CSS-kunnskapen din. Det er en grunnleggende egenskap ved selektor-arkitekturen som gjaldt fra CSS1 helt til 2022.
Hvorfor en kombinator alltid bare ser én vei
Hver eneste kombinator i CSS – mellomrom (etterkommer), > (direkte barn), + (nabo-søsken), ~ (generelt søsken) – beskriver en relasjon mellom to selektorer, men det er alltid elementet til høyre for kombinatoren som stylees. .field input styler input-en inni .field. .field ~ .error styler .error, som er et søsken av .field. Retningen er alltid den samme: fra kontekst til mål, aldri motsatt. Når CSS-motoren møter .field, har den ingen innebygd mekanisme for å «kikke inni» og basert på det endre stylingbeslutningen for .field selv.
Utviklere har i årevis omgått dette på to måter, begge med reelle kostnader. Den første – JavaScript som lytter på input/blur-hendelsen og manuelt legger til klassen .field--invalid på forelderen. Den andre, smartere, men skjørere – det såkalte checkbox/radio-hacket, som bruker ~ til å style søsken basert på :checked-tilstanden, noe som bare fungerer når elementene faktisk er søsken i en flat DOM-struktur, ikke nøstet slik et reelt skjema krever.
jsx
// Løsningen fra før :has() – fungerer, men krever JS til noe// som er en rent visuell konsekvens av HTML-tilstandimport { 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() fjerner denne koden ikke fordi den er et smartere triks – men fordi den løser problemet ved roten, som den første relasjonelle pseudoklassen i CSS-historien, som lar et element spørre om sitt eget indre.
Hvordan :has() egentlig fungerer: en selektor forankret i elementet, ikke i etterkommeren
Det avgjørende tankeskiftet: .field:has(input:invalid) styler ikke input-en. Den styler .field – nøyaktig det elementet pseudoklassen er festet til – forutsatt at det et sted inni finnes et element som matcher selektoren inne i parentesen. Elementet du «forankrer» deg i (eng. anchor element) forblir alltid det samme; :has() avgjør bare om det i det hele tatt får et treff.
css
.field:has(input:invalid) {border-color: #c62828;}.field:has(input:invalid) .field__icon {visibility: visible;}
Som standard leter selektoren inni :has() etter en hvilken som helst etterkommer, på hvilket som helst dybdenivå – akkurat som et vanlig mellomrom. Men du kan innsnevre relasjonen nøyaktig som i vanlig CSS, ved å bruke kombinatorer rett inne i parentesen:
css
/* Kun direkte barn, ikke en hvilken som helst etterkommer */.card:has(> img) {grid-template-columns: 120px 1fr;}/* Element som DIREKTE etterfølges av .error-message som søsken */.field:has(+ .field-hint) {margin-bottom: 4px;}
Dette gjør at :has() ikke er ett nytt triks, men en generalisering – den lar deg uttrykke i CSS enhver relasjon som tidligere kunne beskrives med en kombinator, bare rettet «innover» eller «bakover», i stedet for utelukkende «fremover».
Praktisk eksempel: et skjemafelt som selv vet at det er ugyldig
La oss sette dette sammen til en komplett, tilgjengelig komponent. Et viktig detalj: :invalid i seg selv utløses umiddelbart når et tomt felt med attributtet required lastes – før brukeren har rukket å skrive noe som helst, noe som ville gitt en rød ramme på et tomt, urørt skjema. Dette løses av :not(:placeholder-shown), som bare matcher feltet når brukeren faktisk allerede har lagt igjen noe i det.
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;}/* Feltet er "berørt" (noe er skrevet inn) OG ugyldig – reager først da */.field:has(input:not(:placeholder-shown):invalid) {border-color: #c62828;background: #fff5f5;}.field:has(input:not(:placeholder-shown):invalid) .field__icon {visibility: visible;}/* Positiv bekreftelse er like viktig som en feilmelding */.field:has(input:not(:placeholder-shown):valid) {border-color: #2e7d32;}
Ikke en eneste linje JavaScript ansvarlig for utseendet. Valideringstilstanden finnes allerede nativt i nettleseren – :valid/:invalid har vært der siden CSS3 – :has() lot oss endelig flytte denne eksisterende tilstanden fra input-en til beholderen, dit den visuelt alltid har vært nødvendig.
Det andre mønsteret: en komponent som vet hva den inneholder
Den samme mekanismen løser en helt annen klasse problemer – en layout som avhenger av tilstedeværelsen av bestemt innhold, uten en prop som informerer om det på forhånd.
css
/* Kort med bilde får et topolonneoppsett, kort uten bilde får full tekstbredde */.card:has(> img) {display: grid;grid-template-columns: 96px 1fr;gap: 12px;}/* Seksjonstittelen får bunnmarg bare hvis seksjonen faktisk har en undertittel */.section:has(> .section__subtitle) .section__title {margin-bottom: 4px;}/* En liste uten elementer viser en tom tilstand i stedet for tomrom */.product-list:not(:has(li)) {display: block;}.product-list:not(:has(li))::after {content: "Ingen produkter oppfyller kriteriene.";color: #666;}
Uten :has() ville hvert av disse tilfellene krevd enten en prop (hasImage, isEmpty) satt manuelt av den overordnede komponenten, eller en sjekk av array-lengden i JSX-en og betinget rendering av en egen klasse. :has() lar komponenten selv gjenkjenne sitt eget innhold og reagere på det, i stedet for å bli fortalt om det utenfra – dette er nøyaktig samme tankegang som produktkortet som kjenner sin egen beholder fra artikkelen om Container Queries: mindre state overført manuelt, mer logikk som følger direkte av strukturen.
Fallgruver og gode praksiser
- Spesifisiteten til
:has()er spesifisiteten til den mest spesifikke selektoren inni, ikke null..field:has(input:invalid)har spesifisiteten til to klasser og en pseudoklasse samlet – ikke regn med at:has()«ikke teller» i spesifisiteten, for i praksis overstyrer den regelmessig enklere regler skrevet senere i arket. - Dypt nøstet, bred
:has()uten kombinator kan koste..app:has(.some-deeply-nested-element)tvinger i teorien motoren til å vurdere hele undertreet til.appved hver DOM-endring inni det. Moderne motorer (Chromium, WebKit) optimaliserer dette gjennom såkalte invalidation sets – de regner ikke ut alt blindt – men effekten avhenger av den konkrete selektoren og treets størrelse. Innsnevre relasjonen med en kombinator (>, direkte barn) der det er mulig, i stedet for standard dybdesøk. :has()erstatter ikke:focus-within. Hvis du bare trenger «forelderen reagerer når en etterkommer har fokus», har:focus-withineksistert lenge, er billigere å beregne og mer lesbart –:has(:focus)gir praktisk talt samme effekt, men uten grunn til å gripe til et mer generelt verktøy der et spesialisert allerede finnes.- Nettleserstøtte har sluttet å være et problem.
:has()landet som den siste store brikken i puslespillet – Safari hadde den fra 15.4 (mars 2022), Chrome fra 105 (august 2022), Firefox ble med sist, i versjon 121 (desember 2023). Siden da er den trygg å bruke uten@supportsi ethvert nytt prosjekt.
Oppsummering
I 25 år lot CSS deg bare beskrive relasjoner rettet én vei – fra kontekst til mål, fra forelder til barn, fra forgjenger til etterfølger. :has() er ikke enda en variant av samme idé, men den første pseudoklassen som lar et element spørre om sitt eget indre og reagere på det den finner der – en valideringsfeil i et felt, tilstedeværelsen av et bilde i et kort, fravær av elementer i en liste. Dette flytter en hel klasse av beslutninger som tidligere måtte havne i JavaScript eller i props sendt manuelt ovenfra, tilbake dit de hører hjemme: i HTML-strukturen og CSS-reglene som beskriver den strukturen.