Die CSS :has() Variante für kontextabhängige Styles
CSS :has() ist der erste echte Eltern-Selektor in der Geschichte von CSS. In Tailwind v4 nutzt du ihn direkt im Markup: has-[:checked], has-[input:focus], has-[.error]. Das ermöglicht Styles, die auf den Zustand von Kindselementen reagieren – ohne JavaScript und ohne eigene CSS-Datei.
Inhaltsverzeichnis
- 1. Was CSS :has() grundlegend anders macht
- 2. Syntax der Tailwind has-Variante
- 3. has-[:checked]: Formulare und Toggle-UI
- 4. has-[input:focus]: Fokus-Zustände auf Wrappern
- 5. has-[.klasse]: Komponentenzustände steuern
- 6. Bedingte Layouts mit has in Tailwind
- 7. not-has: Negation des Eltern-Selektors
- 8. group-has und peer-has als Kompositions-Pattern
- 9. has vs. group vs. peer im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Was CSS :has() grundlegend anders macht
Seit den Anfängen von CSS gab es eine fundamentale Einschränkung: CSS kann nur vorwärts selektieren. Ein Selektor wie div p wählt p-Elemente innerhalb eines div, aber niemals ein div basierend darauf, was es enthält. Jahrzehntelang mussten Entwickler für solche Anforderungen JavaScript einsetzen – einen Event-Listener setzen, den DOM traversieren und Klassen manuell hinzufügen. CSS :has() bricht diese Beschränkung auf und ist damit der erste echte Eltern-Selektor in der Geschichte der Sprache.
In Tailwind v4 ist diese Fähigkeit als Tailwind has-Variante direkt im Utility-System verfügbar. has-[:checked] auf einem Container-Element greift, wenn irgendein Nachfahre des Containers den :checked-Zustand hat. has-[input:focus] greift, wenn ein input-Element im Container fokussiert ist. has-[.error] greift, wenn ein Element mit der Klasse error im Container existiert. Diese Tailwind has-Klassen generieren exakt das CSS :has()-Konstrukt im Hintergrund – ohne dass man eine CSS-Datei anfassen muss.
2. Syntax der Tailwind has-Variante
Die Tailwind has-Variante folgt demselben Muster wie andere Tailwind-Modifikatoren: has-[SELEKTOR]:UTILITY. Der Selektor in eckigen Klammern ist ein beliebiger CSS-Selektor, der auf einen Nachfahren des Elements angewendet wird. has-[:checked]:bg-sky-50 setzt den Hintergrund auf sky-50, wenn das Element eine angehakte Checkbox als Nachfahren hat. has-[input:focus]:ring-2 fügt einen Ring hinzu, wenn ein input fokussiert ist. Die Syntax ist flexibel: Typ-Selektoren, Klassen-Selektoren, Attribut-Selektoren und Pseudo-Klassen sind alle möglich.
Kombinationen mit anderen Modifikatoren funktionieren ebenfalls: dark:has-[:checked]:bg-slate-800 für Dark-Mode, sm:has-[img]:grid-cols-2 für responsive bedingte Layouts. In Tailwind v4 kann man die has-Variante auch mit beliebigen Selektoren über die Arbitrary-Syntax erweitern: has-[.product-card:hover] oder has-[input[type='radio']:checked]. Das Ergebnis ist ein vollständig deklarativer Ansatz für CSS, der ohne eine einzige Zeile JavaScript auskommt.
<!-- has-variant syntax examples in Tailwind CSS -->
<!-- Container highlights when any child input is focused -->
<div class="border border-slate-200 rounded-xl p-4
has-[input:focus]:border-sky-500
has-[input:focus]:ring-2
has-[input:focus]:ring-sky-100
transition-all duration-200">
<input type="text" placeholder="Suche..." class="w-full outline-none text-sm text-slate-700">
</div>
<!-- Card changes style when internal checkbox is checked -->
<label class="block border-2 border-slate-200 rounded-2xl p-4 cursor-pointer
has-[:checked]:border-sky-500
has-[:checked]:bg-sky-50
transition-all duration-200">
<input type="radio" name="plan" value="pro" class="sr-only">
<span class="font-semibold text-slate-800 has-[:checked]:text-sky-700">Pro Plan</span>
<p class="text-sm text-slate-500 mt-1">Unbegrenzte Projekte, Priority Support</p>
</label>
<!-- Form wrapper that reacts to validation state -->
<div class="has-[:invalid]:border-red-300 has-[:invalid]:bg-red-50
has-[:valid]:border-emerald-300 has-[:valid]:bg-emerald-50
border-2 rounded-xl p-4 transition-colors duration-200">
<input type="email" required class="w-full outline-none text-sm" placeholder="E-Mail">
</div>
3. has-[:checked]: Formulare und Toggle-UI
Das wohl häufigste Einsatzszenario für Tailwind has ist die Reaktion auf Checkbox- und Radio-Zustände. Das Muster has-[:checked] auf einem Container reagiert darauf, wenn irgendeins seiner Kind-Inputs angehakt oder ausgewählt ist. Das ist leistungsfähiger als der peer-checked-Ansatz, weil has keine bestimmte DOM-Reihenfolge erfordert und tief verschachtelte Inputs erkennt.
Für Pricing-Tables und Optionsauswahl-UIs ist has-[:checked] in Tailwind das perfekte Werkzeug: Jede Optionskarte wird mit einem versteckten Radio-Input versehen. Die Karte selbst bekommt has-[:checked]:ring-2 has-[:checked]:ring-sky-500 has-[:checked]:bg-sky-50. Wenn der Nutzer auf die Karte klickt, wechselt sie visuell in den Ausgewählt-Zustand. Dabei kann das Label (der klickbare Bereich) die gesamte Karte sein – kein JavaScript für Event-Handling, kein Alpine.js-x-model. Die Browser-Formularlogik übernimmt alles.
<!-- Pricing plan selector using has-[:checked] — pure CSS -->
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<!-- Starter plan -->
<label class="relative block border-2 border-slate-200 rounded-2xl p-6 cursor-pointer
has-[:checked]:border-sky-500 has-[:checked]:bg-sky-50 has-[:checked]:shadow-lg
hover:border-slate-300 transition-all duration-200">
<input type="radio" name="pricing" value="starter" class="sr-only" checked>
<!-- Checkmark badge: only visible when selected -->
<div class="absolute top-3 right-3 w-6 h-6 rounded-full bg-sky-500 flex items-center justify-center
opacity-0 has-[:checked]:opacity-100 transition-opacity">
<svg class="w-3.5 h-3.5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"/>
</svg>
</div>
<p class="font-bold text-slate-800 text-lg mb-1">Starter</p>
<p class="text-3xl font-bold text-slate-900 mb-3">€ 0 <span class="text-sm font-normal text-slate-500">/Monat</span></p>
<ul class="text-sm text-slate-600 space-y-1">
<li>1 Projekt</li>
<li>5 GB Speicher</li>
<li>Community Support</li>
</ul>
</label>
<!-- Pro plan -->
<label class="relative block border-2 border-slate-200 rounded-2xl p-6 cursor-pointer
has-[:checked]:border-sky-500 has-[:checked]:bg-sky-50 has-[:checked]:shadow-lg
hover:border-slate-300 transition-all duration-200">
<input type="radio" name="pricing" value="pro" class="sr-only">
<p class="font-bold text-slate-800 text-lg mb-1">Pro</p>
<p class="text-3xl font-bold text-slate-900 mb-3">€ 29 <span class="text-sm font-normal text-slate-500">/Monat</span></p>
<ul class="text-sm text-slate-600 space-y-1">
<li>10 Projekte</li>
<li>50 GB Speicher</li>
<li>E-Mail Support</li>
</ul>
</label>
<!-- Enterprise plan -->
<label class="relative block border-2 border-slate-200 rounded-2xl p-6 cursor-pointer
has-[:checked]:border-sky-500 has-[:checked]:bg-sky-50 has-[:checked]:shadow-lg
hover:border-slate-300 transition-all duration-200">
<input type="radio" name="pricing" value="enterprise" class="sr-only">
<p class="font-bold text-slate-800 text-lg mb-1">Enterprise</p>
<p class="text-3xl font-bold text-slate-900 mb-3">€ 99 <span class="text-sm font-normal text-slate-500">/Monat</span></p>
<ul class="text-sm text-slate-600 space-y-1">
<li>Unbegrenzte Projekte</li>
<li>1 TB Speicher</li>
<li>Priority Support</li>
</ul>
</label>
</div>
4. has-[input:focus]: Fokus-Zustände auf Wrappern
Ein klassisches Problem in der Formulargestaltung: Man möchte den gesamten Wrapper um ein Eingabefeld hervorheben, wenn das Feld fokussiert ist – nicht nur das Feld selbst. Mit :focus-within war das früher möglich, aber das greift für jedes fokussierbare Element innerhalb des Wrappers. Tailwind has bietet mehr Präzision: has-[input:focus] greift nur, wenn speziell ein input-Element fokussiert ist, nicht ein Button oder ein Link im selben Container.
Das ist besonders wertvoll für Suchfelder mit Icons und Shortcut-Anzeigen: Der gesamte Such-Wrapper soll sich hervorheben, wenn der Nutzer in das Suchfeld klickt. Mit has-[input:focus]:border-sky-500 has-[input:focus]:shadow-md auf dem Wrapper passiert genau das – ohne JavaScript, ohne onfocus-Event, ohne Alpine.js. Das Icon im Wrapper kann mit group-has-[input:focus] ebenfalls seinen Zustand wechseln, wenn man den Wrapper zusätzlich als group markiert.
5. has-[.klasse]: Komponentenzustände steuern
Tailwind has kann nicht nur auf Pseudoklassen wie :checked oder :focus reagieren, sondern auf beliebige CSS-Selektoren – einschließlich Klassen-Selektoren. has-[.error] auf einem Formular-Wrapper greift, wenn irgendein Kind-Element die Klasse error hat. Das ist besonders nützlich für Komponentensysteme, wo JavaScript (oder Alpine.js) Klassen setzt und das Layout darauf reagieren soll.
Das Muster: Alpine.js setzt die Klasse error auf ein Validierungs-Element, das im DOM vorhanden ist, wenn ein Fehler aufgetreten ist. Der übergeordnete Formular-Container hat has-[.error]:border-red-300 has-[.error]:bg-red-50. Er reagiert automatisch, ohne dass Alpine.js den Container direkt ansprechen muss. Dieses Muster trennt Verantwortlichkeiten: JavaScript verwaltet den Zustand (Klasse setzen/entfernen), CSS reagiert auf den Zustand (Styles anwenden). Die Tailwind has-Variante ist dabei die Brücke zwischen beiden Welten.
<!-- has-[.klasse] reacts to dynamic class from Alpine.js -->
<div x-data="{ hasError: false, value: '' }"
class="border-2 rounded-2xl p-6 transition-all duration-200
has-[.field-error]:border-red-300
has-[.field-error]:bg-red-50
has-[.field-success]:border-emerald-300
has-[.field-success]:bg-emerald-50">
<label class="block text-sm font-semibold text-slate-700 mb-2">Benutzername</label>
<input type="text"
x-model="value"
@input="hasError = value.length > 0 && value.length < 3"
class="w-full border border-slate-300 rounded-xl px-4 py-2.5 text-sm outline-none
focus:border-sky-400 focus:ring-2 focus:ring-sky-100">
<!-- Error element: class field-error triggers has-[.field-error] on parent -->
<p x-show="hasError && value.length > 0"
class="field-error mt-2 text-xs text-red-600 font-medium">
Mindestens 3 Zeichen erforderlich.
</p>
<!-- Success element: class field-success triggers has-[.field-success] -->
<p x-show="!hasError && value.length >= 3"
class="field-success mt-2 text-xs text-emerald-600 font-medium">
Benutzername ist verfügbar.
</p>
</div>
6. Bedingte Layouts mit has in Tailwind
Eine der überraschendsten Anwendungen der Tailwind has-Variante ist die bedingte Layout-Steuerung. Ein Grid-Container kann sein Layout wechseln, wenn er ein bestimmtes Kind-Element enthält: has-[img]:grid-cols-[1fr_2fr] schaltet auf ein zweispaltiges Layout um, wenn das Element ein Bild enthält. has-[aside]:grid-cols-[280px_1fr] fügt eine Sidebar-Spalte hinzu, wenn ein aside-Element vorhanden ist.
Dieses Muster ermöglicht flexible CMS-Layouts, bei denen Redakteure entscheiden, ob ein Bild oder eine Sidebar eingebunden wird – und das Layout passt sich automatisch an, ohne dass Templates angepasst werden müssen. In Magento-Hyvä-Layouts ist das besonders interessant: Ein Produkt-Container kann automatisch ein anderes Layout annehmen, wenn die Produktgalerie oder die Konfigurations-Optionen im DOM vorhanden sind. Die Tailwind has-Variante reagiert auf den DOM-Zustand, nicht auf eine explizite Klasse.
7. not-has: Negation des Eltern-Selektors
Tailwind unterstützt neben has-[...] auch die Negation: not-has-[...] oder über die Kombination has-[]:not-[...]. Das ist nützlich für Styles, die nur dann gelten sollen, wenn kein bestimmtes Kind vorhanden ist. Ein leerer Zustand – ein Container ohne Listenelemente – kann mit not-has-[li]:flex not-has-[li]:items-center not-has-[li]:justify-center als zentrierter Leerbereich dargestellt werden, der sich automatisch in ein normales Layout verwandelt, sobald Elemente hinzukommen.
In Tailwind v4 schreibt man die Negation als [&:not(:has([...]))]:utility oder nutzt die native not-has-Variante, falls im Projekt konfiguriert. Das ist ein fortgeschrittenes Muster, aber für CMS-Inhalte und dynamische Listen extrem hilfreich: Der "Keine Ergebnisse"-Zustand und der normale Listen-Zustand unterscheiden sich durch Klassen auf demselben Container – ohne JavaScript, das den Zustand ermitteln und Klassen setzen muss. Die Tailwind has-Variante macht den DOM-Zustand selbst zur Wahrheitsquelle für das Styling.
8. group-has und peer-has als Kompositions-Pattern
Tailwind has lässt sich mit group und peer kombinieren, um Styles auf beliebige Elemente im Komponentenbaum zu übertragen. Das Muster group-has-[input:checked]:... auf einem Kind-Element reagiert darauf, ob das nächste übergeordnete group-Element irgendwo einen angehakten Input enthält. Das ist mächtiger als group-has allein, weil der zu stylende Knoten beliebig tief im Kind-Baum liegen kann.
Ein Beispiel: Eine komplexe Formularkarte, bei der ein Bestätigungs-Text am unteren Ende erscheinen soll, wenn irgendwo in der Karte eine bestimmte Option ausgewählt ist. Der Bestätigungs-Text liegt in keiner direkten Geschwister-Beziehung zum Radio-Input (kein peer möglich) und ist auch kein direktes Kind (kein einfaches has). Mit group auf der Karte und group-has-[:checked]:block auf dem Bestätigungs-Text löst man das sauber: Die Karte beobachtet als Gruppe ihren gesamten Kindsbaum, und der Bestätigungs-Text reagiert auf den Gruppen-Zustand.
<!-- group + has combination: complex multi-level state propagation -->
<div class="group border border-slate-200 rounded-2xl overflow-hidden">
<div class="p-6">
<h3 class="font-bold text-slate-800 mb-4">Lieferoption wählen</h3>
<div class="space-y-3">
<label class="flex items-center gap-3 cursor-pointer">
<input type="radio" name="shipping" value="standard" class="peer accent-sky-500">
<span class="text-sm text-slate-700 peer-checked:font-semibold peer-checked:text-sky-700">
Standardversand (3–5 Tage) — kostenlos
</span>
</label>
<label class="flex items-center gap-3 cursor-pointer">
<input type="radio" name="shipping" value="express" class="peer accent-sky-500">
<span class="text-sm text-slate-700 peer-checked:font-semibold peer-checked:text-sky-700">
Expressversand (1–2 Tage) — € 4,99
</span>
</label>
<label class="flex items-center gap-3 cursor-pointer">
<input type="radio" name="shipping" value="same-day" class="peer accent-sky-500">
<span class="text-sm text-slate-700 peer-checked:font-semibold peer-checked:text-sky-700">
Same-Day-Lieferung — € 12,99
</span>
</label>
</div>
</div>
<!-- This confirmation bar is hidden until ANY radio in the group is checked -->
<!-- group-has-[:checked] watches the entire group's subtree -->
<div class="hidden group-has-[:checked]:flex items-center gap-3 px-6 py-3 bg-sky-50 border-t border-sky-100">
<svg class="w-4 h-4 text-sky-600 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
<p class="text-sm text-sky-700 font-medium">Lieferoption ausgewählt. Weiter zum nächsten Schritt.</p>
</div>
</div>
9. has vs. group vs. peer im direkten Vergleich
Die drei Tailwind-Mechanismen für zustandsbasierte Styles ergänzen sich, haben aber unterschiedliche Stärken. Die Wahl hängt von der DOM-Struktur und der Richtung der Zustandspropagation ab.
| Mechanismus | Selektor-Typ | DOM-Anforderung | Typisches Einsatzszenario |
|---|---|---|---|
| has-[...] | Eltern reagiert auf Kind | Kein – Kind kann beliebig tief sein | Container auf Checkbox, Input-Fokus, Fehlerklasse reagieren |
| group-hover | Eltern → Kind (Pseudo-Zustand des Elterns) | group auf Elternelement setzen | Hover-Kaskaden, Overlay bei Karten, Navigationsmenüs |
| peer-checked | Geschwister → nachfolgendes Geschwister | Peer-Ziel muss nach Peer im HTML stehen | Checkbox als Toggle, Floating Labels, Akkordeons |
| group-has | Eltern beobachtet Kind, steuert beliebiges Kind | group auf Container, group-has-[...] auf Ziel | Bestätigungs-Elemente, die nicht direkte Geschwister sind |
| not-has-[...] | Eltern reagiert auf Abwesenheit eines Kinds | Kein | Leerzustände, fallback-Layouts ohne bestimmte Elemente |
Die Faustregel für die Wahl: has-[...] direkt auf einem Element ist die erste Wahl, wenn das Element selbst seinen Stil basierend auf dem Zustand eines Kindes wechseln soll. group-has kommt zum Einsatz, wenn das zu stylierende Element weit vom beobachteten Element entfernt ist und man über einen gemeinsamen Eltern-Container kommunizieren muss. peer-checked bleibt die einfachste Option für direkte Geschwister-Beziehungen.
Mironsoft
Tailwind CSS v4 · Hyvä Themes · Alpine.js
Moderne CSS-Features, richtig eingesetzt?
Wir nutzen Tailwind has, group und peer konsequent, um interaktive Interfaces zu bauen, die minimal auf JavaScript angewiesen sind – schneller, robuster, einfacher zu warten.
has-Analyse
Bestehende JavaScript-Zustände auf has/group/peer-Eignung prüfen
Formular-UI
Formulare mit has-Validierung, Floating Labels und CSS-only Feedback bauen
Magento-Hyvä
Produktseiten und Checkout-UI mit Tailwind has optimieren
10. Zusammenfassung
Die Tailwind has-Variante bringt CSS :has() in das Utility-First-System und macht damit den ersten echten Eltern-Selektor der CSS-Geschichte für Tailwind-Entwickler zugänglich. has-[:checked] für Optionskarten und Pricing-Tables, has-[input:focus] für Wrapper-Hervorhebungen, has-[.error] für die Reaktion auf Alpine.js-gesetzte Klassen – all das funktioniert ohne JavaScript, ohne Event-Listener und ohne manuelle DOM-Manipulation. Die DOM-Struktur selbst ist die Wahrheitsquelle.
Im Zusammenspiel mit group-has und peer-has entstehen Kompositions-Patterns, die komplexe Zustandspropagation über beliebige DOM-Tiefen ermöglichen. Der praktische Vorteil: Weniger JavaScript, kleinere Bundles, bessere Performance. Gleichzeitig bleibt die Deklarativität des Tailwind-Ansatzes erhalten – kein dynamisches Klassen-Setzen in JavaScript, kein Auseinanderdriften von Styles und Logik. Die Tailwind has-Variante ist eines der wichtigsten Features von Tailwind v4 und wird in modernen Projekten schnell unverzichtbar.
Tailwind has — Das Wichtigste auf einen Blick
Syntax
has-[SELEKTOR]:UTILITY. Jeder CSS-Selektor in eckigen Klammern: has-[:checked], has-[input:focus], has-[.error], has-[img].
Wann has statt peer?
has wenn das Element sich selbst stylen soll. peer wenn ein nachfolgendes Geschwister gesteuert werden soll. group-has für tief verschachtelte Ziele.
Browser-Unterstützung
Chrome 105+, Firefox 121+, Safari 15.4+. Alle modernen Browser seit 2022/2023. Für Legacy-Support auf group/peer zurückgreifen.
Kombination
Kombinierbar mit dark:, sm:, lg: und anderen Modifikatoren. Auch group-has-[...] und peer-has-[...] für Cross-Element-Kommunikation verfügbar.