CSS :has() — föräldern som vet vad som händer inuti
Du har ett formulärfält: <div class="field"> med en etikett och en input inuti. Kravet låter banalt – när input:en är ogiltig ska hela behållaren få en röd ram och en varningsikon bredvid etiketten, inte bara själva input:en. Du tar till .field input:invalid – och kör huvudet i väggen. Den selektorn skulle stylat input:en, om du velat ge den en röd ram. Men du vill stylea .field, alltså input:ens förälder, utifrån vad som händer i dess inre. Och plötsligt visar det sig att CSS – trots dussintals kombinatorer, pseudoklasser och attributselektorer – aldrig under sina 25 år har erbjudit ett sätt att göra det.
Det är inte en lucka i din CSS-kunskap. Det är ett grundläggande drag i selektorernas arkitektur, som gällde från CSS1 ända fram till 2022.
Varför en kombinator alltid bara tittar åt ett håll
Varje kombinator i CSS – mellanslag (ättling), > (direkt barn), + (närmaste syskon), ~ (allmänt syskon) – beskriver en relation mellan två selektorer, men det är alltid elementet till höger om kombinatorn som stylas. .field input stylar input:en inuti .field. .field ~ .error stylar .error, som är syskon till .field. Riktningen är alltid densamma: från kontext till mål, aldrig tvärtom. CSS-motorn har, när den stöter på .field, ingen inbyggd mekanism för att "kika in" och utifrån det ändra beslutet om stylingen av .field självt.
Utvecklare har i åratal kringgått detta på två sätt, båda med verkliga kostnader. Det första – JavaScript som lyssnar på händelserna input/blur och manuellt lägger till klassen .field--invalid på föräldern. Det andra, smartare men skörare – det så kallade checkbox/radio-tricket, som använder ~ för att stylea syskon utifrån :checked-tillstånd, vilket bara fungerar om elementen faktiskt är syskon i en platt DOM-struktur, och inte nästlade så som ett riktigt formulär kräver.
jsx
// Lösningen före :has() – fungerar, men kräver JS för något// som är en rent visuell konsekvens av HTML-tillståndetimport { 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() eliminerar den koden inte för att den är ett smartare trick – utan för att den löser problemet vid källan, som den första relationella pseudoklassen i CSS historia som låter ett element fråga om sitt eget inre.
Hur :has() egentligen fungerar: en selektor förankrad i elementet, inte i ättlingen
Den avgörande tankeförskjutningen: .field:has(input:invalid) stylar inte input:en. Den stylar .field – exakt det element som pseudoklassen sitter på – förutsatt att det någonstans i dess inre finns ett element som matchar selektorn inom parentesen. Elementet du "förankrar" på (det så kallade anchor element) förblir alltid detsamma; :has() avgör bara om det överhuvudtaget blir en träff.
css
.field:has(input:invalid) {border-color: #c62828;}.field:has(input:invalid) .field__icon {visibility: visible;}
Som standard letar selektorn inuti :has() efter vilken ättling som helst, på vilket djup som helst – precis som ett vanligt mellanslag. Men du kan begränsa relationen exakt som i vanlig CSS, genom att använda kombinatorer direkt inuti parentesen:
css
/* Bara ett direkt barn, inte vilken ättling som helst */.card:has(> img) {grid-template-columns: 120px 1fr;}/* Ett element som OMEDELBART följs av .field-hint som syskon */.field:has(+ .field-hint) {margin-bottom: 4px;}
Det gör att :has() inte är ett enda nytt trick, utan en generalisering – den låter dig uttrycka varje relation i CSS som tidigare gick att beskriva med en kombinator, fast riktad "inåt" eller "bakåt" i stället för enbart "framåt".
Praktiskt exempel: ett formulärfält som själv vet att det är ogiltigt
Låt oss sätta ihop det till en komplett, tillgänglig komponent. En viktig detalj: :invalid i sig utlöses direkt när ett tomt fält med attributet required laddas – innan användaren hunnit skriva något, vilket skulle ge en röd ram på ett tomt, orört formulär. Det löses av :not(:placeholder-shown), som bara matchar fältet när användaren faktiskt redan lämnat något 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;}/* Fältet är "berört" (något har skrivits) OCH ogiltigt – reagera först då */.field:has(input:not(:placeholder-shown):invalid) {border-color: #c62828;background: #fff5f5;}.field:has(input:not(:placeholder-shown):invalid) .field__icon {visibility: visible;}/* Positiv bekräftelse är lika viktig som ett fel */.field:has(input:not(:placeholder-shown):valid) {border-color: #2e7d32;}
Inte en enda rad JavaScript ansvarig för utseendet. Valideringstillståndet finns redan nativt i webbläsaren – :valid/:invalid har funnits sedan CSS3 – :has() gjorde det äntligen möjligt att flytta det befintliga tillståndet från input:en till dess behållare, dit det visuellt alltid borde ha hört.
Ett andra mönster: en komponent som vet vad den innehåller
Samma mekanism löser en helt annan klass problem – layout som beror på förekomsten av visst innehåll, utan en prop som talar om det i förväg.
css
/* Ett kort med bild får en tvåkolumnslayout, ett kort utan bild får full textbredd */.card:has(> img) {display: grid;grid-template-columns: 96px 1fr;gap: 12px;}/* Sektionsrubriken får marginal nedåt bara om sektionen faktiskt har en undertitel */.section:has(> .section__subtitle) .section__title {margin-bottom: 4px;}/* En lista utan ett enda element visar ett tomt-tillstånd i stället för tomt utrymme */.product-list:not(:has(li)) {display: block;}.product-list:not(:has(li))::after {content: "Inga produkter matchar kriterierna.";color: #666;}
Utan :has() skulle vart och ett av de här fallen kräva antingen en prop (hasImage, isEmpty) som sätts manuellt av den överordnade komponenten, eller en kontroll av arrayens längd i JSX och villkorlig rendering av en separat klass. :has() låter komponenten själv känna igen sitt eget innehåll och reagera, i stället för att bli informerad om det utifrån – exakt samma tankesätt som produktkortet, medvetet om sin egen container, i artikeln om Container Queries: mindre state som skickas för hand, mer logik som följer direkt av strukturen.
Fallgropar och god praxis
- Specificiteten hos
:has()är specificiteten hos den mest specifika selektorn inuti, inte noll..field:has(input:invalid)har specificiteten av två klasser och en pseudoklass tillsammans – räkna inte med att:has()"inte räknas" i specificiteten, för i praktiken slår den regelbundet enklare regler som skrivs senare i stilmallen. - Ett djupt nästlat, brett
:has()utan kombinator kan kosta..app:has(.some-deeply-nested-element)tvingar teoretiskt motorn att överväga hela underträdet till.appvid varje DOM-ändring i dess inre. Moderna motorer (Chromium, WebKit) optimerar det via så kallade invalidation sets – de räknar inte om allt blint – men effekten beror på den specifika selektorn och trädets storlek. Begränsa relationen med en kombinator (>, direkt barn) där det går, i stället för att som standard söka på djupet. :has()ersätter inte:focus-within. Om du bara behöver "föräldern reagerar när en ättling har fokus" finns:focus-withinsedan länge, är billigare att beräkna och mer läsbart –:has(:focus)ger praktiskt taget samma effekt, men utan anledning att ta till ett mer generellt verktyg där ett specialiserat redan finns.- Webbläsarstöd har slutat vara ett problem.
:has()blev den sista stora pusselbiten på plats – Safari hade den från 15.4 (mars 2022), Chrome från 105 (augusti 2022), Firefox anslöt sist, i version 121 (december 2023). Sedan dess är den säker att använda utan@supportsi alla nya projekt.
Sammanfattning
I 25 år lät CSS dig bara beskriva relationer riktade åt ett håll – från kontext till mål, från förälder till barn, från föregångare till efterföljare. :has() är inte ännu en variant av samma idé, utan den första pseudoklassen som låter ett element fråga om sitt eget inre och reagera på vad det hittar där – ett valideringsfel i ett fält, förekomsten av en bild i ett kort, avsaknaden av element i en lista. Det flyttar en hel klass av beslut som tidigare var tvungna att hamna i JavaScript eller i props som skickades ner manuellt, tillbaka dit de hör hemma: till HTML-strukturen och de CSS-regler som beskriver den strukturen.