verschachtelte Zustandsketten ohne zusätzliches JavaScript
Einfache group-hover- und peer-focus-Muster kennt jeder, der mit Tailwind arbeitet. Sobald aber mehrere Ebenen verschachtelt sind, mehrere Kindzustände nach oben durchgereicht werden müssen oder ein Formular auf den Zustand eines völlig anderen Elements reagieren soll, reichen die Basics nicht mehr aus. Named groups, group-has, peer-has und Datenattribut-Zustandsketten lösen genau diese fortgeschrittenen Group und Peer State Kombinationen.
Inhaltsverzeichnis
- 1. Warum einfache Group und Peer State Kombinationen an Grenzen stoßen
- 2. Die Selektor-Mechanik hinter group und peer kurz aufgefrischt
- 3. Named Groups: group/name und peer/name für Eindeutigkeit
- 4. group-has() und peer-has(): Kindzustände nach oben reichen
- 5. Zustandsketten: hover, focus-within und Datenattribute kombinieren
- 6. State Machines mit Datenattributen und Alpine.js
- 7. Mehrere Group-Ebenen gleichzeitig verschachteln
- 8. Debugging: DOM-Reihenfolge, Browser-Support und typische Fehler
- 9. Group und Peer Patterns im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum einfache Group und Peer State Kombinationen an Grenzen stoßen
Die Basis von group-hover: und peer-checked: reicht für die meisten UI-Aufgaben aus: eine Karte, die beim Hover ihren Schatten verstärkt, oder ein Label, das sich verfärbt, wenn eine Checkbox aktiv ist. Sobald aber zwei Gruppen ineinander verschachtelt sind, etwa eine Tabellenzeile innerhalb einer Tabelle innerhalb eines Panels, kollidieren die einfachen group-*-Selektoren. Der Browser kann ohne zusätzliche Angabe nicht unterscheiden, auf welche der mehreren übergeordneten Gruppen sich ein group-hover: bezieht.
Genau hier setzen fortgeschrittene Group und Peer State Kombinationen an. Named groups lösen die Mehrdeutigkeit bei Verschachtelung, group-has() und peer-has() erlauben es, den Zustand eines beliebigen Nachfahren statt nur des direkten Elements abzufragen, und Datenattribut-Zustandsketten machen aus einzelnen Hover-Effekten kleine, deklarative State Machines. Wer diese Werkzeuge kennt, kann komplexe Interaktionsmuster in reinem CSS abbilden, für die früher zwingend JavaScript-Event-Listener nötig waren.
Der Praxisnutzen zeigt sich vor allem in Formularen, Dashboards und Navigationskomponenten mit mehreren voneinander abhängigen Elementen. Ein Akkordeon, dessen Pfeil sich dreht, wenn irgendein Kindelement fokussiert ist, oder eine Sidebar, die sich abhängig vom Zustand eines entfernten Peer-Elements verhält, sind typische Fälle für fortgeschrittene Group und Peer State Kombinationen, die im weiteren Verlauf dieses Artikels Schritt für Schritt aufgebaut werden.
2. Die Selektor-Mechanik hinter group und peer kurz aufgefrischt
Bevor die fortgeschrittenen Muster Sinn ergeben, lohnt sich ein kurzer Blick auf die Grundmechanik. group markiert ein Elternelement, group-hover: auf einem Nachfahren generiert intern einen CSS-Selektor wie .group:hover .child. peer markiert stattdessen ein vorangehendes Geschwisterelement, peer-checked: generiert .peer:checked ~ .sibling. Der entscheidende Unterschied: peer funktioniert nur bei Geschwistern, die im DOM nach dem Peer-Element stehen, niemals davor.
Diese reine CSS-Mechanik bedeutet, dass alle Group und Peer State Kombinationen ohne jedes JavaScript funktionieren, solange der native CSS-Zustand (:hover, :focus, :checked, :disabled) den gewünschten Trigger liefert. Erst wenn ein Zustand nicht nativ existiert, etwa ein aktiver Tab oder ein geöffnetes Menü, kommen Datenattribute wie data-state="open" als Trigger ins Spiel, kombiniert mit den data-*-Varianten von Tailwind. Diese Grundmechanik bleibt für alle folgenden fortgeschrittenen Muster identisch, nur die Komplexität der generierten Selektoren steigt.
3. Named Groups: group/name und peer/name für Eindeutigkeit
Sobald zwei group-Elemente ineinander verschachtelt sind, führt group-hover: ohne Namen immer zum äußersten passenden group. Named groups lösen das, indem der Gruppenname direkt an die Klasse gehängt wird: group/card definiert die Gruppe, group-hover/card: reagiert ausschließlich auf genau diese benannte Gruppe, unabhängig davon, wie viele weitere group-Elemente dazwischenliegen. Derselbe Mechanismus existiert für peer/name und peer-checked/name:.
In der Praxis empfiehlt es sich, Namen an die fachliche Rolle statt an die HTML-Struktur zu binden, etwa group/row für eine Tabellenzeile und group/panel für das umgebende Panel. Diese Konvention macht Group und Peer State Kombinationen in größeren Komponenten sofort lesbar, ohne dass man den HTML-Baum mitzählen muss, um zu verstehen, auf welche Ebene sich ein Selektor bezieht. Named groups sind reine Zusatzinformation im Klassennamen, sie erzeugen keinen zusätzlichen DOM-Knoten und keine Laufzeitkosten.
<!-- Nested groups: without a name, group-hover would always target the outermost group -->
<div class="group/panel rounded-2xl border border-slate-200 p-4">
<p class="text-slate-500 group-hover/panel:text-sky-700">Panel title</p>
<div class="group/row mt-3 flex items-center justify-between rounded-lg p-2
hover:bg-slate-50 group-hover/panel:border-sky-200">
<span class="group-hover/row:font-semibold">Order #4821</span>
<!-- This icon reacts only to the row, never to the outer panel hover -->
<svg class="w-4 h-4 opacity-0 group-hover/row:opacity-100 transition-opacity"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 5l7 7-7 7"/>
</svg>
</div>
</div>
4. group-has() und peer-has(): Kindzustände nach oben reichen
Die native CSS-Pseudoklasse :has() erlaubt es erstmals, ein Elternelement basierend auf dem Zustand eines Nachfahren zu stylen, was in reinem CSS lange Zeit unmöglich war. Tailwind bringt dafür die Varianten group-has-*: und peer-has-*:, die intern zu .group:has(...) beziehungsweise .peer:has(...) kompiliert werden. Damit lassen sich Group und Peer State Kombinationen bauen, die vorher zwingend JavaScript erforderten: ein Formularfeld-Container, der sich rot färbt, sobald irgendein Kind-Input im Fehlerzustand ist, oder ein Akkordeon-Header, der sich hervorhebt, wenn irgendein internes Element fokussiert wird.
Die Syntax kombiniert has-[...] mit einem beliebigen CSS-Selektor in eckigen Klammern, etwa group-has-[:checked]: oder group-has-[[data-invalid]]:. Dadurch lässt sich exakt definieren, welcher Nachfahren-Zustand relevant ist, ohne dass jeder mögliche Kind-Zustand einzeln im Elternelement dupliziert werden muss. Für häufige Fälle bietet Tailwind zusätzlich Kurzformen wie group-has-checked:, die intern denselben Selektor erzeugen, aber ohne die eckige Klammer auskommen.
<!-- Form section highlights itself as soon as ANY nested input is invalid -->
<fieldset class="group rounded-xl border-2 border-slate-200 p-4
has-[[data-invalid]]:border-red-400 has-[[data-invalid]]:bg-red-50">
<legend class="font-semibold group-has-[[data-invalid]]:text-red-700">
Zahlungsdaten
</legend>
<input type="text" name="iban" data-invalid
class="mt-2 w-full rounded-lg border px-3 py-2">
<p class="hidden group-has-[[data-invalid]]:block text-sm text-red-600 mt-1">
Bitte eine gueltige IBAN eingeben.
</p>
</fieldset>
<!-- Peer variant: a summary line reacts to a checkbox nested inside a sibling -->
<div class="peer rounded-lg border p-3">
<label><input type="checkbox" class="mr-2">Newsletter abonnieren</label>
</div>
<p class="text-sm text-slate-500 peer-has-checked:text-sky-700 peer-has-checked:font-semibold">
Aktualisiert automatisch, sobald die Checkbox markiert wird.
</p>
5. Zustandsketten: hover, focus-within und Datenattribute kombinieren
Der eigentliche Mehrwert fortgeschrittener Group und Peer State Kombinationen entsteht, wenn mehrere Zustände gleichzeitig auf verschiedene Weisen ausgewertet werden. Ein typisches Muster: Ein Element soll hervorgehoben werden, wenn entweder der Nutzer mit der Maus darüber ist, ODER wenn ein Kindelement fokussiert wurde, ODER wenn ein externer Zustand über ein Datenattribut gesetzt ist. Tailwind erlaubt es, diese Bedingungen einfach als mehrere Klassen mit demselben Zielwert zu notieren, da CSS mehrere zutreffende Regeln ohnehin additiv anwendet.
Wichtig für die Lesbarkeit von Zustandsketten ist eine konsistente Reihenfolge in den Klassenlisten: zuerst die Basiszustände (hover:, focus:), dann Gruppen- und Peer-Varianten, zuletzt Datenattribut-Varianten. Diese Konvention macht es für andere Entwickler leichter, auf einen Blick zu erkennen, welche Group und Peer State Kombinationen ein Element beeinflussen, ohne die komplette Klassenliste im Kopf durchspielen zu müssen. Bei mehr als vier bis fünf kombinierten Zuständen lohnt sich meist eine Extraktion in eine wiederverwendbare Komponentenklasse über @apply oder eine eigene Utility.
6. State Machines mit Datenattributen und Alpine.js
Zustände, die kein natives CSS-Pendant haben, etwa ein dreiwertiger Tab-Status (inaktiv, aktiv, geladen) oder ein mehrstufiger Wizard, lassen sich über Alpine.js in ein Datenattribut schreiben und von dort aus mit Tailwinds data-*-Varianten stylen. Alpine setzt dabei typischerweise x-bind:data-state="currentStep === 2 ? 'active' : 'idle'", und Tailwind reagiert mit data-[state=active]:bg-sky-600. Diese Kombination macht aus einer imperativen JavaScript-Zustandsverwaltung eine deklarative, direkt im Markup lesbare State Machine.
Der entscheidende Vorteil gegenüber reinem x-show oder bedingten Klassenlisten in JavaScript: Die eigentliche Stildefinition bleibt vollständig in Tailwind-Klassen, Alpine liefert nur den Wert des Datenattributs. Damit lassen sich Group und Peer State Kombinationen mit echten mehrwertigen Zuständen bauen, nicht nur mit binären An/Aus-Zuständen wie bei :hover oder :checked. Das reduziert Duplizierung erheblich, weil ein einziges Datenattribut mehrere Elemente gleichzeitig steuern kann.
<!-- Alpine writes a three-way state into a data attribute,
Tailwind styles purely from that attribute -->
<div x-data="{ step: 1 }" class="space-y-2">
<template x-for="n in [1, 2, 3]" :key="n">
<button
type="button"
@click="step = n"
:data-state="step === n ? 'active' : (step > n ? 'done' : 'idle')"
class="w-full text-left px-4 py-2 rounded-lg border transition-colors
data-[state=idle]:border-slate-200 data-[state=idle]:text-slate-500
data-[state=active]:border-sky-500 data-[state=active]:bg-sky-50 data-[state=active]:font-semibold
data-[state=done]:border-green-300 data-[state=done]:text-green-700"
x-text="'Schritt ' + n">
</button>
</template>
</div>
7. Mehrere Group-Ebenen gleichzeitig verschachteln
In komplexen Dashboards treten häufig drei oder mehr verschachtelte Gruppenebenen gleichzeitig auf: eine Seiten-Sidebar, darin eine Kategorie, darin ein einzelner Menüpunkt. Jede Ebene braucht einen eigenen Namen, damit Group und Peer State Kombinationen nicht versehentlich auf die falsche Ebene reagieren. Die Empfehlung: Jede Named-Group-Ebene bekommt einen kurzen, eindeutigen Namen, der die semantische Rolle beschreibt, etwa group/sidebar, group/category und group/item.
Ein häufiger Fehler bei tiefen Verschachtelungen ist, dass ein inneres Element versehentlich auf eine äußere Gruppe reagiert, weil der Name vergessen wurde und Tailwind auf den nächstgelegenen unbenannten group-Vorfahren zurückfällt. Ein konsequentes Review, bei dem jede group-*:-Klasse mit einem expliziten Namen versehen wird, verhindert dieses stille Fehlverhalten zuverlässig und macht die verschachtelten Group und Peer State Kombinationen vorhersehbar wartbar, auch wenn später weitere Verschachtelungsebenen hinzukommen.
<!-- Three nested group levels, each with an explicit name -->
<aside class="group/sidebar w-64">
<div class="group/category" data-open>
<p class="group-hover/category:text-sky-700">Berichte</p>
<a href="/reports/sales"
class="group/item block px-3 py-1.5 rounded-md
group-hover/category:bg-slate-50
hover:bg-sky-100 group-hover/item:font-semibold">
<span class="group-hover/item:text-sky-700">Umsatzbericht</span>
</a>
</div>
</aside>
8. Debugging: DOM-Reihenfolge, Browser-Support und typische Fehler
Der häufigste Fehler bei peer-*:-Varianten ist eine falsche DOM-Reihenfolge. Das mit peer markierte Element muss im Markup vor dem reagierenden Geschwisterelement stehen, da CSS ausschließlich den allgemeinen Geschwister-Kombinator ~ unterstützt, niemals rückwärts. Ein Label, das vor einer Checkbox steht, kann deshalb niemals über peer-checked: auf diese Checkbox reagieren, wenn die Checkbox erst danach im DOM folgt. Die Lösung ist, das visuelle Layout notfalls mit order oder Flex-Direction umzukehren, während die DOM-Reihenfolge für die Peer-Logik korrekt bleibt.
Bezüglich Browser-Support ist :has() seit Ende 2023 in allen modernen Evergreen-Browsern implementiert, was group-has-*: und peer-has-*: für die allermeisten Projekte produktionstauglich macht. Ein zweiter typischer Fehler bei fortgeschrittenen Group und Peer State Kombinationen ist ein vergessenes relative oder ein falsch positioniertes group, das mehrere unabhängige Komponenten gleichzeitig umschließt, statt genau eine. Die Browser-DevTools zeigen im Element-Inspector unter "Computed" verlässlich, welcher generierte Selektor tatsächlich zutrifft, das ist der schnellste Weg, eine falsche Verschachtelung zu finden.
9. Group und Peer Patterns im direkten Vergleich
Je nach Anwendungsfall eignet sich ein anderes Muster aus dem Werkzeugkasten der Group und Peer State Kombinationen besser. Die folgende Übersicht ordnet die wichtigsten Varianten nach typischem Einsatzzweck.
| Muster | Trigger-Element | Typischer Einsatz |
|---|---|---|
| group-hover | Elternelement, direkte Nachfahren | Karten-Icons, Hover-Reveal |
| peer-checked | Vorangehendes Geschwister | Custom Checkbox/Radio-Styles |
| group/name | Benannte, verschachtelte Gruppe | Mehrere Group-Ebenen gleichzeitig |
| group-has([...]) | Beliebiger Nachfahre | Formular-Validierung, Fehlermarkierung |
| data-[state=...] | Extern gesetztes Attribut | Mehrwertige State Machines mit Alpine |
In der Praxis kombinieren größere Komponenten meist mehrere dieser Muster gleichzeitig, etwa named groups für die Struktur und Datenattribute für den fachlichen Zustand. Wichtig bleibt, jede Ebene bewusst zu benennen und die DOM-Reihenfolge für peer-Varianten konsequent einzuhalten, damit die Kombinationen langfristig wartbar bleiben.
Mironsoft
Tailwind Interaktionsmuster und Hyvä Frontend-Architektur
Komplexe Interaktionen ohne zusätzliches JavaScript?
Wir bauen verschachtelte Group und Peer State Kombinationen, Formular-Validierungslogik mit has() und datenbasierte State Machines, sauber in Tailwind-Klassen statt in verstreuten Event-Listenern.
Interaktions-Audit
Analyse bestehender Event-Listener auf CSS-only-Alternativen
Komponenten-Refactoring
Named groups und has()-Selektoren fuer verschachtelte UI
Alpine-Integration
Datenattribut-State-Machines fuer mehrwertige Zustaende
10. Zusammenfassung
Fortgeschrittene Group und Peer State Kombinationen erweitern die einfachen group-hover:- und peer-checked:-Muster um drei zentrale Werkzeuge: named groups fuer eindeutige Verschachtelung, has()-Varianten fuer die Reaktion auf beliebige Nachfahren-Zustaende, und Datenattribute fuer mehrwertige State Machines, die ueber binaere CSS-Pseudoklassen hinausgehen. Zusammen decken diese Muster einen Großteil der Interaktionsfälle ab, für die frueher zwingend JavaScript-Event-Listener geschrieben werden mussten.
Der entscheidende Vorteil liegt in der Wartbarkeit: Die gesamte Interaktionslogik bleibt direkt im Markup sichtbar, statt in separaten Skriptdateien verstreut zu sein. Wer named groups konsequent benennt, has()-Selektoren gezielt statt inflationär einsetzt und Datenattribute als klar dokumentierte Zustandswerte behandelt, baut Komponenten, die auch nach Monaten noch nachvollziehbar sind, ohne dass man den kompletten JavaScript-Code durchsuchen muss, um eine einzelne Stilregel zu verstehen.
Group und Peer State Kombinationen advanced — Das Wichtigste auf einen Blick
Named Groups
group/name und group-hover/name: loesen Mehrdeutigkeit bei verschachtelten Gruppen zuverlaessig auf.
has()-Varianten
group-has-[...]: und peer-has-[...]: reagieren auf beliebige Nachfahren-Zustaende, nicht nur direkte Kinder.
Datenattribut-State-Machines
data-[state=...]: kombiniert mit Alpine.js fuer mehrwertige, deklarative Zustaende.
DOM-Reihenfolge
Peer-Varianten funktionieren ausschliesslich vorwaerts im DOM, niemals rueckwaerts zum vorherigen Geschwister.