Fast drei Jahrzehnte lang konnte CSS ein Element anhand seiner Vorfahren, seiner Geschwister oder seiner eigenen Attribute ansprechen, aber nie anhand seines Inhalts. Eine <section> je nachdem, ob sie ein Bild enthält oder nicht, unterschiedlich zu gestalten, erforderte bisher eine per JavaScript umgeschaltete Klasse. Die Pseudoklasse :has() schließt diese Lücke: Seit Dezember 2023 in den wichtigsten Browsern nativ verfügbar, erlaubt sie es, ein Elternelement oder ein vorausgehendes Element anhand dessen auszuwählen, was es enthält oder was ihm folgt.
Die Grundsyntax
:has() nimmt eine Liste relativer Selektoren als Argument und trifft auf das Element zu, wenn mindestens einer dieser Selektoren darunter oder daneben eine Übereinstimmung findet. Man kann es sich als das CSS-Äquivalent einer „Lookahead“-Assertion in regulären Ausdrücken vorstellen: Man wählt ein Element anhand dessen aus, was es begleitet, ohne dieses begleitende Element jemals direkt anzusprechen.
Ein Element anhand dessen ansprechen, was ihm folgt
Kombiniert mit einem Geschwister-Kombinator erlaubt :has(), auf ein nachfolgendes Element zu reagieren – etwas, das CSS bisher schlicht nicht konnte:
/* Verringert den Abstand einer h1, auf die direkt eine h2 folgt */
h1:has(+ h2) {
margin-bottom: 0.25rem;
}
/* Kombiniert mit :is() über mehrere Überschriftenebenen */
:is(h1, h2, h3):has(+ :is(h2, h3, h4)) {
margin-bottom: 0.25rem;
}Das ist einer der am häufigsten genannten Anwendungsfälle: der Abstand zwischen einer Überschrift und der direkt folgenden Unterüberschrift wird automatisch verringert, ohne dass für jedes Vorkommen eine Utility-Klasse ins Markup eingefügt werden muss.
Formulare und Interface-Komponenten
Der verbreitetste Anwendungsfall in Produktivumgebungen betrifft Formulare und interaktive Komponenten, wo JavaScript bisher einzig dazu diente, eine Statusklasse umzuschalten. In Kombination mit Container Queries, die eine Komponente bereits an die Größe ihres Containers anpassen, geht :has() noch weiter und passt sie auch an ihren Inhalt an:
/* Hebt ein ungültiges Formularfeld hervor */
.form-field:has(input:invalid) {
border-color: var(--color-danger);
}
/* Ändert das Aussehen einer Produktkarte, wenn eine Checkbox aktiviert ist */
.product-card:has(input[type="checkbox"]:checked) {
outline: 2px solid var(--accent);
}
/* UND-Logik: beide Bedingungen müssen zutreffen */
body:has(video):has(audio) {
--has-rich-media: 1;
}Einen leeren Zustand ohne JavaScript erkennen
Ein weiterer häufiger Anwendungsfall in Produktivumgebungen ist die Anzeige einer Leer-Status-Meldung, wenn eine Liste keine Einträge enthält – eine Aufgabe, die bisher das Zählen der Kindelemente serverseitig oder in JavaScript erforderte. Kombiniert mit :not() lässt sich diese Abwesenheit mit :has() direkt ausdrücken:
/* Zeigt eine Standardmeldung, wenn .list kein .item enthält */
.list:not(:has(.item)) .empty-state {
display: block;
}
.list:has(.item) .empty-state {
display: none;
}Dasselbe Prinzip gilt für einen leeren Warenkorb, ein Postfach ohne Nachrichten oder ein Dashboard ohne konfiguriertes Widget: Fälle, in denen die Bedingung „dieses Element enthält nichts“ bisher eine nach jeder DOM-Aktualisierung ausgeführte JavaScript-Prüfung war und jetzt deklarativ bleiben und sich automatisch mit dem Inhalt aktualisieren kann.
Browser-Unterstützung
:has() gehört zu den „Baseline“-Funktionen, also solchen, die seit Dezember 2023 ohne Präfix oder Aktivierung in allen wichtigen Browsern verfügbar sind. Die Unterstützung umfasst sowohl Desktop- als auch mobile Versionen von:
| Browser | Mindestversion |
|---|---|
| Chrome / Edge | 105 und neuer |
| Safari (Desktop und iOS) | 15.4 und neuer |
| Firefox | 121 und neuer |
| Opera | 91 und neuer |
Für ein Projekt, das ältere Versionen dieser Browser noch unterstützen muss, erlaubt ein Fähigkeitstest über @supports selector(:has(a)) einen sauberen Fallback, statt ein ganzes Stylesheet-Regelwerk stillschweigend scheitern zu lassen.
Spezifität und Grenzen, die man kennen sollte
:has() übernimmt die Spezifität des spezifischsten übergebenen Selektors, genau wie :is() und :not(). Zwei Einschränkungen sollte man vor einem breiten Einsatz im Kopf behalten: Es kann nicht in ein anderes :has() verschachtelt werden, und Pseudoelemente sind innerhalb seiner Klammern keine gültigen Selektoren, was zyklische bedingte Abhängigkeiten verhindert.
Die häufigste Kompatibilitätsfalle: Wird :has() vom Browser nicht unterstützt, scheitert der gesamte Selektorblock stillschweigend, nicht nur der :has()-Teil. Für einen sichereren Fallback in Code, der noch auf älteren Browsern laufen muss, begrenzt eine Einbettung in :is() oder :where() die Nebenwirkungen, und ein Unterstützungstest über @supports selector(:has(a)) bleibt die ausdrücklichste Methode.
Die Performance-Falle, die es zu vermeiden gilt
Weil :has() die Rendering-Engine zwingt, die Nachfahren oder Geschwister eines Elements zu prüfen, bevor entschieden wird, ob es passt, kann eine zu breit gefasste Verankerung auf großen Seiten teure Durchläufe durch ganze Teilbäume auslösen.
:has() verlagert eine Logik nach CSS, die bisher das Abhören von Events in JavaScript erforderte, nur um eine Statusklasse umzuschalten.
Wann JavaScript weiterhin nötig bleibt
:has() ersetzt nicht alles. Interaktionen, die von einem Anwendungszustand, asynchronen Daten oder Geschäftslogik abhängen, bleiben weiterhin Sache von JavaScript; ebenso stützen sich Entscheidungen für ein helles oder dunkles Theme weiterhin auf Design Tokens statt auf eine Inhaltsauswahl. Alles jedoch, wobei es nur darum ging, eine Klasse hinzuzufügen oder zu entfernen, um den internen Zustand des DOM widerzuspiegeln – ein ausgefülltes Feld, eine aktivierte Checkbox, ein vorhandenes oder fehlendes Kindelement –, wird zu einem natürlichen Kandidaten für :has(), mit weniger zu wartendem JavaScript und weniger Risiko, dass die Klasse vom tatsächlichen DOM-Zustand abweicht.
Was man sich merken sollte
:has() ist seit Dezember 2023 nativ in allen aktuellen Versionen der wichtigsten Browser verfügbar und kann schon jetzt einen guten Teil des Utility-JavaScripts für die Verwaltung von Statusklassen ersetzen. Sein Einsatz erfordert jedoch dieselbe Disziplin wie jeder kombinierte Selektor: Verankerung an einem präzisen Container, Einschränkung des inneren Teils mit direkten Kombinatoren und Beschränkung von :has() auf Fälle, in denen die Beziehung zwischen Element und Inhalt tatsächlich strukturell ist.
Das ist genau die Art von CSS-Funktion, die keinen Lärm macht, aber still und leise einen guten Teil der Komponenten-Gewohnheiten verändert, sobald man sie einmal einsetzt. In meinen letzten Projekten habe ich mehrere kleine JavaScript-Schnipsel, die nur eine Statusklasse umgeschaltet haben, durch ein einfaches :has() ersetzt, mit einem klaren Gewinn an Lesbarkeit. Das einzige wirkliche Risiko ist der bequeme Missbrauch zu breiter Selektoren: In der Entwicklung auf einer schlanken Seite funktioniert das hervorragend, und es zeigt sich deutlich weniger gut, sobald die Seite mit echtem Inhalt gefüllt ist – Simon Janvier.
Zum Weiterlesen: die MDN-Dokumentation zum Selektor :has().
