Formulare, Empty States und Theme-Logik ohne JavaScript
Wer den has-Selektor nur als eltern-selektierendes Werkzeug kennt, nutzt einen Bruchteil seines Potenzials. Diese Sammlung zeigt konkrete Rezepte für Formularvalidierung, leere Zustände, Quantity Queries und Theme-Umschaltung, die zuvor zwingend JavaScript brauchten und jetzt reines CSS sind.
Inhaltsverzeichnis
- 1. Warum der has-Selektor mehr ist als ein Eltern-Selektor
- 2. Syntax kurz wiederholt, dann direkt in die Praxis
- 3. Rezept: Formularvalidierung ohne JavaScript
- 4. Rezept: Leere Zustände zuverlässig erkennen
- 5. Rezept: Quantity Queries nach Kind-Anzahl
- 6. Rezept: Geschwister-Kontext mit Kombinatoren
- 7. Rezept: Theme-Umschaltung ohne Body-Klassen
- 8. Performance und Browser-Support in der Praxis
- 9. has-Selektor im Vergleich mit JavaScript-Alternativen
- 10. Zusammenfassung
- 11. FAQ
1. Warum der has-Selektor mehr ist als ein Eltern-Selektor
Der has-Selektor wird meistens als der lang ersehnte Eltern-Selektor vorgestellt: article:has(img) selektiert jeden Artikel, der ein Bild enthält. Das ist zutreffend, greift aber deutlich zu kurz. Tatsächlich ist der has-Selektor eine relationale Pseudoklasse, die prüft, ob innerhalb ihres Arguments irgendein Selektor auf einen Nachfahren, ein Geschwister oder eine Kombination davon zutrifft. Dieses Argument kann jeder beliebige Selektor sein, inklusive Kombinatoren, Attributselektoren und weiterer Pseudoklassen wie :not() oder :checked.
Diese Flexibilität macht den has-Selektor zu einem der mächtigsten Werkzeuge, die CSS in den letzten Jahren bekommen hat. Vieles, was früher zwingend eine JavaScript-Klasse am Elternelement brauchte, etwa form.has-error oder body.dark-mode, lässt sich jetzt direkt im Stylesheet ausdrücken. Der Zustand eines Nachfahren wird zur Bedingung für das Styling eines Vorfahren, ganz ohne dass JavaScript den DOM manipuliert. Die folgenden Abschnitte zeigen konkrete Rezepte, die über die reine Eltern-Selektion hinausgehen und den has-Selektor als vollwertiges Zustandswerkzeug einsetzen.
2. Syntax kurz wiederholt, dann direkt in die Praxis
Die Grundsyntax des has-Selektor ist schnell erklärt: selektor:has(argument) trifft zu, wenn innerhalb von selektor mindestens ein Element existiert, das dem Argument entspricht. Das Argument ist dabei relativ zum Element mit :has() zu lesen, nicht absolut zum Dokument. li:has(> a) nutzt den Kind-Kombinator, um nur direkte Kinder zu prüfen, während li:has(a) auch tiefer verschachtelte Links erfasst. Dieser Unterschied entscheidet in der Praxis häufig darüber, ob ein Rezept präzise funktioniert oder unerwartet zu viele Elemente trifft.
Wichtig für alle folgenden Rezepte: der has-Selektor kann mehrfach verkettet und mit anderen Selektoren kombiniert werden. form:has(input:invalid):not(:has(input:placeholder-shown)) ist ein gültiger, wenn auch komplexer Ausdruck. Je verschachtelter der Ausdruck, desto wichtiger wird Lesbarkeit, deshalb lohnt es sich, komplexe has-Selektor Ausdrücke in Custom Properties oder gut kommentierte Regelblöcke aufzuteilen, statt sie in einer einzigen langen Zeile unterzubringen.
/* Basic has() recap: parent gets a state from its children */
article:has(img) {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 1.5rem;
}
/* Direct child combinator narrows the match precisely */
li:has(> a.external) {
border-left: 3px solid #7c3aed;
}
/* Combined with :not() for exclusion logic */
form:has(input:invalid):not(:has(input:focus)) {
outline: 2px solid #dc2626;
outline-offset: 4px;
}
3. Rezept: Formularvalidierung ohne JavaScript
Formularvalidierung ist das Paradebeispiel, an dem der has-Selektor seinen Wert beweist. Ohne :has() musste JavaScript den Fehlerstatus eines Feldes prüfen und eine Klasse am umgebenden Container setzen, damit etwa das Label rot eingefärbt oder ein Hinweistext eingeblendet werden konnte. Mit dem has-Selektor reicht ein einziger Selektor: .field:has(input:invalid) label { color: #dc2626; } färbt das Label automatisch ein, sobald der native Validierungsstatus des Inputs invalid ist, ganz ohne Submit-Event und ohne JavaScript-Listener.
Noch nützlicher wird das Rezept, wenn man :placeholder-shown mit einbezieht, um Fehler erst nach einer Eingabe anzuzeigen, statt sofort beim Laden der Seite. .field:has(input:invalid:not(:placeholder-shown)) zeigt Fehlerstyling nur dann, wenn der Nutzer bereits etwas eingetippt hat und das Ergebnis ungültig ist. Diese Kombination aus dem has-Selektor und nativen Pseudoklassen ersetzt viel der Validierungslogik, die früher zwingend JavaScript brauchte, und bleibt dabei vollständig deklarativ im Stylesheet.
/* Show error styling only after the user has typed something */
.field:has(input:invalid:not(:placeholder-shown)) label {
color: #dc2626;
font-weight: 600;
}
.field:has(input:invalid:not(:placeholder-shown))::after {
content: "Bitte gültigen Wert eingeben";
display: block;
font-size: 0.8rem;
color: #dc2626;
}
/* Success state without any JavaScript validation logic */
.field:has(input:valid:not(:placeholder-shown)) input {
border-color: #16a34a;
}
/* Submit button reacts to overall form validity */
form:has(input:invalid) button[type="submit"] {
opacity: 0.5;
pointer-events: none;
}
4. Rezept: Leere Zustände zuverlässig erkennen
Ein zweites praxisrelevantes Rezept für den has-Selektor ist das Erkennen leerer Container. Warenkorb-Widgets, Suchergebnislisten und Benachrichtigungszentren müssen häufig anders aussehen, wenn sie keine Einträge enthalten, etwa mit einem Platzhaltertext und einem anderen Padding. Bisher wurde dieser Zustand meist per Template-Bedingung im Backend oder per JavaScript-Zählung der Kindelemente entschieden. Mit dem has-Selektor lässt sich der leere Zustand direkt im CSS abfragen: ul.cart-items:not(:has(li)) trifft nur zu, wenn die Liste keine einzige Zeile enthält.
Der entscheidende Kniff ist die Kombination aus :not() und dem has-Selektor: statt zu prüfen, ob etwas vorhanden ist, wird geprüft, ob nichts vorhanden ist, und genau dieser negierte Zustand bekommt sein eigenes Styling. Das funktioniert unabhängig davon, ob die Liste serverseitig gerendert oder per Fetch nachträglich befüllt wird, solange am Ende der reale DOM-Zustand entscheidet. Für komplexere Container mit mehreren möglichen Kindtypen lässt sich das Rezept erweitern, etwa .results:not(:has(.result-item, .result-card)), um mehrere mögliche Eintragstypen gleichzeitig abzudecken.
/* Empty cart: no <li> children present at all */
ul.cart-items:not(:has(li)) {
display: grid;
place-items: center;
padding: 3rem 1rem;
}
ul.cart-items:not(:has(li))::before {
content: "Ihr Warenkorb ist leer";
color: #6b7280;
font-size: 0.95rem;
}
/* Works for multiple possible item types at once */
.search-results:not(:has(.result-item, .result-card)) {
min-height: 12rem;
background: #f8fafc;
border-radius: 0.75rem;
}
5. Rezept: Quantity Queries nach Kind-Anzahl
Quantity Queries bezeichnen eine Technik, bei der sich das Styling nach der Anzahl der vorhandenen Kindelemente richtet, etwa ein anderes Grid-Layout bei drei Elementen als bei sieben. Vor dem has-Selektor gelang das nur mit umständlichen, schwer wartbaren Kombinationen aus :nth-child und :nth-last-child. Mit dem has-Selektor und dem allgemeinen Geschwister-Kombinator wird die Zählung deutlich lesbarer: .grid:has(> :nth-child(4)) trifft zu, sobald mindestens vier direkte Kinder existieren, weil ein viertes Kind vorhanden sein muss, damit der Selektor überhaupt greift.
Dieses Muster lässt sich zu einer vollständigen Quantity Query ausbauen, indem man die untere und obere Grenze kombiniert. .grid:has(> :nth-child(4)):not(:has(> :nth-child(7))) beschreibt exakt den Bereich zwischen vier und sechs Kindelementen. Der has-Selektor macht diese Bedingung deklarativ und ohne JavaScript-Zählung ausdrückbar, was besonders für responsive Grid-Layouts nützlich ist, bei denen die Spaltenzahl von der Anzahl der Karten abhängen soll, nicht nur von der Viewport-Breite.
/* Fewer than 4 items: single column, larger cards */
.grid:not(:has(> :nth-child(4))) {
grid-template-columns: 1fr;
}
/* Between 4 and 6 items: two columns */
.grid:has(> :nth-child(4)):not(:has(> :nth-child(7))) {
grid-template-columns: repeat(2, 1fr);
}
/* 7 or more items: dense three-column grid */
.grid:has(> :nth-child(7)) {
grid-template-columns: repeat(3, 1fr);
gap: 0.75rem;
}
6. Rezept: Geschwister-Kontext mit Kombinatoren
Der has-Selektor entfaltet seine volle Stärke in Kombination mit dem allgemeinen Geschwister-Kombinator ~. Damit lässt sich ein Element abhängig von einem später im Markup folgenden Geschwister stylen, was vorher schlicht unmöglich war, weil CSS-Kombinatoren traditionell nur vorwärts wirken. Ein praktisches Beispiel: eine Checkbox weiter oben im Formular soll ein Feld weiter unten optisch hervorheben, sobald sie aktiviert ist. Mit input[type="checkbox"]:checked ~ .conditional-field ließ sich das bereits vorher lösen, aber nur, wenn das Zielelement ein direktes Geschwister war.
Der has-Selektor erweitert das auf beliebig verschachtelte Strukturen: .form-section:has(~ .form-section input:checked) reagiert auf eine Checkbox, die irgendwo in einer späteren Sektion liegt, unabhängig von der Verschachtelungstiefe. Diese Rückwärtsreferenz war der eigentliche Grund, warum Entwickler jahrelang einen Eltern-Selektor gefordert haben, und der has-Selektor löst genau dieses Problem eleganter, als es die ursprüngliche Forderung je vorsah.
7. Rezept: Theme-Umschaltung ohne Body-Klassen
Viele Projekte schalten zwischen hellem und dunklem Design um, indem JavaScript eine Klasse wie dark-mode am body-Element setzt. Mit dem has-Selektor lässt sich derselbe Effekt erzielen, ohne dass JavaScript überhaupt eine Klasse verwalten muss: eine versteckte Checkbox oder ein natives details-Element hält den Zustand, und body:has(#theme-toggle:checked) reagiert direkt darauf. Der Vorteil liegt darin, dass der Zustand vollständig im HTML lebt und beim Laden der Seite sofort korrekt ist, ohne auf JavaScript-Hydration warten zu müssen.
Dieses Rezept eignet sich besonders für Prototypen und für Projekte, in denen JavaScript bewusst minimal gehalten wird. Der has-Selektor ersetzt hier keine vollwertige Theme-Verwaltung mit Systempräferenz-Erkennung, aber für einfache Umschalter zwischen zwei oder drei Varianten ist die Lösung robust, performant und unabhängig von Skript-Ladezeiten. Kombiniert mit prefers-color-scheme als Default lässt sich ein vollständiges, größtenteils CSS-natives Theme-System aufbauen.
/* State lives in the checkbox, no JavaScript class management needed */
body:has(#theme-toggle:checked) {
--bg: #0f172a;
--fg: #f1f5f9;
}
body:not(:has(#theme-toggle:checked)) {
--bg: #ffffff;
--fg: #0f172a;
}
body {
background: var(--bg);
color: var(--fg);
transition: background 0.2s ease, color 0.2s ease;
}
8. Performance und Browser-Support in der Praxis
Ein berechtigtes Bedenken bei jedem neuen Selektor ist die Rendering-Performance. Der has-Selektor muss theoretisch Nachfahren prüfen, um eine Aussage über den Vorfahren zu treffen, was aufwendiger klingt als ein einfacher Klassenvergleich. In der Praxis haben Browser-Engines dafür interne Optimierungen eingebaut, etwa Invalidierungssets, die nur bei relevanten DOM-Änderungen neu auswerten, statt bei jedem Repaint den kompletten Teilbaum zu durchsuchen. Für die überwiegende Mehrheit realer Anwendungsfälle, wie Formulare mit einigen Dutzend Feldern oder Karten-Grids mit einigen Dutzend Elementen, ist der Unterschied nicht messbar.
Kritisch wird es erst bei sehr großen, tief verschachtelten Bäumen mit häufigen DOM-Mutationen, etwa virtualisierten Listen mit tausenden Zeilen, die der has-Selektor in jeder Sektion neu evaluieren müsste. Hier lohnt sich ein Performance-Profiling im Browser, bevor man den Selektor produktiv einsetzt. Beim Browser-Support gilt: alle aktuellen Evergreen-Browser unterstützen den has-Selektor seit 2023, ältere Browser benötigen ein Fallback per Feature Detection mit @supports selector(:has(a)), damit das Layout dort nicht bricht, sondern degradiert.
9. has-Selektor im Vergleich mit JavaScript-Alternativen
Nicht jedes der gezeigten Rezepte ersetzt JavaScript vollständig, aber in vielen Fällen ist die CSS-Lösung robuster, weil sie unabhängig von Skript-Ladezeiten funktioniert. Die folgende Tabelle stellt die typischen Ansätze gegenüber und zeigt, wo der has-Selektor die klar bessere Wahl ist und wo JavaScript weiterhin notwendig bleibt.
| Anwendungsfall | JavaScript-Ansatz | has-Selektor Rezept | Empfehlung |
|---|---|---|---|
| Formularvalidierung | Event-Listener setzt Fehlerklasse | :has(input:invalid) |
CSS ausreichend |
| Empty States | Kinder zählen, Klasse setzen | :not(:has(li)) |
CSS ausreichend |
| Quantity Queries | Array-Länge auswerten | :has(> :nth-child(n)) |
CSS ausreichend |
| Serverseitige Formularlogik | Backend-Validierung, Session | Nicht abbildbar | JavaScript/Backend nötig |
| Persistenter Theme-State | localStorage, Systempräferenz | Nur einfache Umschaltung | Kombination beider Ansätze |
Der has-Selektor ersetzt JavaScript also nicht generell, sondern genau dort, wo der Zustand ausschließlich aus dem DOM selbst ableitbar ist. Sobald Persistenz über einen Seitenaufruf hinaus oder serverseitige Logik nötig wird, bleibt JavaScript oder das Backend zuständig, während der has-Selektor die reine Präsentationslogik übernimmt.
Mironsoft
Modernes CSS, Hyvä-Themes und wartbare Frontend-Architektur
CSS-Selektoren, die JavaScript ersetzen, statt es zu duplizieren?
Wir analysieren bestehende Frontend-Logik und ersetzen unnötiges JavaScript durch robuste, moderne CSS-Selektoren wie den has-Selektor, für schnellere Ladezeiten und wartbareren Code.
CSS-Audit
Bestehende Stylesheets auf unnötiges JavaScript und veraltete Muster prüfen
Formular-Refactoring
Validierungslogik auf native Pseudoklassen und has-Selektor umstellen
Hyvä-Integration
Moderne Selektoren sauber in Tailwind- und Alpine-Komponenten einbinden
10. Zusammenfassung
Der has-Selektor ist deutlich mehr als ein Eltern-Selektor: er ist ein relationales Zustandswerkzeug, das den DOM-Zustand von Nachfahren und Geschwistern in Styling-Bedingungen übersetzt. Formularvalidierung, Empty States, Quantity Queries und einfache Theme-Umschaltung lassen sich damit deklarativ im Stylesheet lösen, ohne dass JavaScript den DOM manipulieren oder Klassen verwalten muss. Die Kombination mit :not(), dem Kind-Kombinator und dem allgemeinen Geschwister-Kombinator eröffnet Muster, die vorher schlicht nicht ausdrückbar waren.
Wichtig bleibt, den has-Selektor gezielt einzusetzen: dort, wo der Zustand vollständig aus dem aktuellen DOM ableitbar ist, ist er die robustere und schnellere Lösung. Sobald Persistenz über die Seite hinaus oder serverseitige Logik nötig wird, bleibt eine Kombination mit JavaScript sinnvoll. Wer die gezeigten Rezepte kennt, kann in den meisten Projekten sofort mehrere Zeilen JavaScript durch wenige Zeilen CSS ersetzen.
has-Selektor Rezepte: Das Wichtigste auf einen Blick
Formulare
:has(input:invalid:not(:placeholder-shown)) zeigt Fehler erst nach Eingabe, komplett ohne JavaScript-Validierung.
Empty States
:not(:has(li)) erkennt leere Container zuverlässig, unabhängig davon, wie sie befüllt werden.
Quantity Queries
:has(> :nth-child(n)) macht die Kindanzahl zur Styling-Bedingung, lesbarer als reine nth-child-Ketten.
Performance
Für typische UI-Größen unkritisch, bei sehr großen Bäumen vorher profilen und Fallback per @supports einplanen.