Der CSS :has()-Selektor: Ein praktischer Leitfaden
Wie die Pseudoklasse :has() CSS endlich einen Eltern-Selektor gibt – samt Mustern, Fallstricken und Performance-Kompromissen.

Zwei Jahrzehnte lang war es der running gag in CSS, dass es keinen Eltern-Selektor gibt. Man konnte ein Kind anhand seines Elternelements stylen, aber nie umgekehrt – "style diese .card, wenn sie ein img enthält" erforderte JavaScript, ein Class-Toggle oder einen Workaround, der in Wahrheit nicht funktionierte. Die Pseudoklasse :has(), mittlerweile in jedem evergreen Browser verfügbar, schließt diese Lücke endlich. Sie ist kein Eltern-Selektor im wörtlichen Sinn – sie ist eine relationale Pseudoklasse –, aber in der Praxis tut sie genau das, was Entwickler seit CSS2 wollten: die Struktur des DOM Styling-Entscheidungen treffen lassen, ohne JavaScript anzufassen.
Der Haken dabei: :has() ist mächtig genug, um missbraucht zu werden. Es liest sich wie eine simple "enthält dies das" Prüfung, ist aber tatsächlich ein vollständiger Selektor, der relational ausgewertet wird – und das hat echte Konsequenzen für deine CSS-Architektur und deine Render-Performance.
Was :has() wirklich macht
:has() matcht ein Element, wenn mindestens einer der übergebenen Selektoren auf etwas relativ zu diesem Element zutrifft – ein Nachfahre, ein Geschwisterelement oder ein direktes Kind. Die MDN-Dokumentation zu :has() nennt es genau deshalb eine "relationale Pseudoklasse": Sie selektiert nicht das Element in den Klammern, sondern das Subjekt – basierend darauf, was sich darin befindet.
/* Style a card differently if it contains an image */
.card:has(img) {
display: grid;
grid-template-columns: 120px 1fr;
}
/* Style a form group when its input is invalid */
.form-group:has(input:invalid) {
border-left: 3px solid var(--color-danger);
}
/* Style a label based on a sibling checkbox's state */
.toggle-row:has(input[type="checkbox"]:checked) {
background: var(--color-surface-active);
}Achte auf das dritte Beispiel: :has() ist nicht auf Nachfahren beschränkt. Kombiniert mit dem allgemeinen Sibling-Kombinator ~ lässt es dich auf den Zustand eines Geschwisterelements reagieren – etwas, das vorher nur möglich war, wenn man das Markup umstrukturierte, um den "checkbox hack" zu missbrauchen.
Muster aus der Praxis
Das Muster, das in Produktionscode am häufigsten auftaucht, ist bedingtes Layout basierend auf der Anzahl der Inhalte. Vor :has() bedeutete das einen ResizeObserver in JavaScript oder eine serverseitig gerenderte Klasse basierend auf der Array-Länge.
/* ❌ Bad: JavaScript just to add a layout class */
const grid = document.querySelector('.card-grid');
if (grid.children.length > 4) {
grid.classList.add('dense-layout');
}/* ✅ Good: pure CSS, no observer, no re-render */
.card-grid:has(> .card:nth-child(5)) {
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
}Das zweite Muster ist validierungsbewusstes Styling, ohne dass eine Validierungs-Library deine DOM-Klassen anfasst:
/* Highlight the whole fieldset if any required field is empty */
fieldset:has(input:required:placeholder-shown) {
outline: 2px dashed var(--color-warning);
}
/* Disable a submit button declaratively when a form is invalid */
button[type="submit"]:has(+ .form-errors:not(:empty)) {
pointer-events: none;
opacity: 0.5;
}Ein dritter gängiger Anwendungsfall ist auf Inhalte skopiertes Theming, bei dem eine Komponente wissen muss, ob sie ein bestimmtes Kindelement hat, ohne eine Build-Time- oder Runtime-Klasse:
/* Give articles with a hero image more generous top spacing */
article:has(> .hero-image) {
padding-block-start: 0;
}
article:not(:has(> .hero-image)) {
padding-block-start: 2rem;
}Genau dieses letzte Paar – :has() kombiniert mit :not() – ist der Punkt, an dem die Pseudoklasse beginnt, ganze Klassen bedingter Rendering-Logik zu ersetzen, die früher in deinem Komponentencode lebten.
Performance-Überlegungen
:has() ist nicht kostenlos. Da es anhand von Nachfahren matchen kann, würde eine naive Implementierung den Browser zwingen, bei jeder Style-Neuberechnung den gesamten Teilbaum jedes Kandidatenelements zu durchlaufen. Browser-Hersteller haben das mit internen Optimierungen abgefedert – indem sie nachverfolgen, welche Elemente "has-anchors" sind, und Styles inkrementell invalidieren statt das DOM erneut zu durchsuchen –, aber das Kostenmodell unterscheidet sich trotzdem von einem einfachen Nachfahren-Selektor.
Ein paar Faustregeln, die in echten Codebasen zählen:
- Scope die rechte Seite von
:has()so eng wie möglich..card:has(img)ist günstig..card:has(* img)oder tief verschachtelte Selektoren innerhalb von:has()kosten mehr, weil der Browser einen größeren Teil des Teilbaums prüfen muss. - Vermeide
:has()auf Elementen, die sich ständig neu rendern, etwa Listenelemente in einem virtualisierten Scroll-Container, wo jede Mutation eine relationale Neuprüfung auslösen kann. - Bevorzuge
:has()gegenüber einemMutationObservernur, wenn die Prüfung einfach ist. Musst du auf komplexe Zustandsänderungen reagieren (API-Antworten, berechnete Werte), sind JavaScript-gesteuerte Klassen nach wie vor das richtige Werkzeug –:has()löst strukturelle und zustandsbasierte Bedingungen, keine Business-Logik.
/* ❌ Expensive: unbounded descendant search */
.sidebar:has(.nav-item.active .submenu a:hover) {
box-shadow: inset 4px 0 0 var(--color-accent);
}
/* ✅ Cheap: shallow, direct relationship */
.sidebar:has(> .nav-item.active) {
box-shadow: inset 4px 0 0 var(--color-accent);
}Wenn du ein bestehendes Stylesheet auf :has()-Performance überprüfst, schau dir im Performance-Panel der DevTools das "Style recalculation"-Timing vor und nach dem Hinzufügen eines relationalen Selektors an – die Kosten sind oft vernachlässigbar, solange der Teilbaum nicht zu tief oder die Elementanzahl nicht zu groß wird.
Browser-Support und Progressive Enhancement
:has() ist in CSS Selectors Level 4 definiert und wird Stand jetzt in allen großen evergreen Browsern unterstützt – Chromium-basierte Browser, Safari und Firefox. Allerdings bedeutet "evergreen" nicht "der Browser jedes Nutzers", besonders wenn du Enterprise-Umgebungen mit älteren Installationen unterstützen musst.
Das sichere Muster ist Feature-Detection mit @supports, was funktioniert, weil :has() von Anfang an darauf ausgelegt wurde, erkennbar zu sein:
@supports selector(:has(a)) {
.card:has(img) {
grid-template-columns: 120px 1fr;
}
}
@supports not selector(:has(a)) {
/* Fallback: always show the two-column layout */
.card {
grid-template-columns: 120px 1fr;
}
}So bekommst du ein Layout, das graceful degradiert, statt stillschweigend zu versagen. Wenn dein Fallback mehr braucht als eine Layout-Anpassung – etwa das Deaktivieren eines Submit-Buttons – behalte die JavaScript-basierte Prüfung als Sicherheitsnetz bei, statt dich bei allem, was die Formular-Korrektheit betrifft, allein auf CSS zu verlassen.
:has() mit anderen Selektoren kombinieren
Die wahre Stärke zeigt sich, wenn :has() mit anderen Features aus Selectors Level 4 kombiniert wird, insbesondere :not() und Container Queries. Ein Muster, zu dem ich oft greife: ein Layout sowohl anhand seines Inhalts als auch seines verfügbaren Platzes stylen.
.content-area {
container-type: inline-size;
}
/* Only widen the sidebar gap if the container is roomy
AND the sidebar actually has nested navigation */
@container (min-width: 640px) {
.layout:has(.sidebar .submenu) {
grid-template-columns: 280px 1fr;
}
}Diese Art der Komposition – strukturelles Bewusstsein (:has()) plus räumliches Bewusstsein (Container Queries) – ist das, was Komponenten-CSS wirklich eigenständig macht, ohne dass eine übergeordnete Komponente Layout-Props durchreichen muss, nur um die Präsenz eines Kindes zu berücksichtigen.
Die wichtigsten Punkte
:has()ist eine relationale Pseudoklasse, kein Eltern-Selektor im wörtlichen Sinn – es matcht das Subjekt basierend auf den Nachfahren, Kindern oder Geschwistern, die du innerhalb der Klammern angibst.- Nutze es, um JavaScript zu ersetzen, das nur existiert, um eine Klasse basierend auf DOM-Struktur oder Formular-Zustand umzuschalten – das ist sein stärkster Anwendungsfall.
- Halte den Selektor innerhalb von
:has()so flach wie möglich; unbegrenzte Nachfahren-Prüfungen verursachen echte Kosten bei der Style-Neuberechnung. - Umhülle unkritische Anwendungen mit
@supports selector(:has(a))und behalte JavaScript-Fallbacks für alles, was Formular-Korrektheit oder Barrierefreiheit betrifft. - Kombiniere
:has()mit:not()und Container Queries, um Komponenten zu bauen, die sowohl auf ihren Inhalt als auch auf ihren verfügbaren Platz reagieren, ohne Prop Drilling. - Überprüfe jede
:has()-Ergänzung an einem häufig mutierenden Teilbaum (virtualisierte Listen, Live-Feeds) vor dem Deployment mit dem Performance-Panel der DevTools.


