Skip to content

Türchen 12 – State Styling ohne JavaScript

Published: at 07:00 AM

Interaktivität war lange Zeit fest in der Hand von JavaScript. Für viele UI-Zustände wie z.B. Dropdowns, Akkordeons, aktive Navigationselemente, Validierungen galt: Ohne JavaScript geht es nicht.

Doch moderne CSS-Selektoren wie :checked, :focus-within, :target und vor allem :has() haben das Spielfeld verändert. Sie ermöglichen Zustandslogik direkt im CSS, ohne zusätzliche Skripte. Das macht Komponenten einfacher, robuster und oft sogar performanter.

Dieses Türchen zeigt, wie man statebasierte Logik rein mit CSS umsetzt und wo die Grenzen und Chancen liegen.

Der Schlüssel: Selektoren, die auf Interaktion reagieren

Die wichtigsten Werkzeuge für zustandsbasiertes CSS sind moderne Pseudoklassen und Attribute-Selektoren, die auf Benutzerinteraktionen und Browser-Zustände reagieren. Der :checked-Selektor ermöglicht es, auf aktivierte Checkboxen und Radio-Buttons zu reagieren und ist damit die Grundlage für viele Toggle-Mechanismen. :focus-within geht einen Schritt weiter als das einfache :focus und reagiert nicht nur, wenn ein Element selbst den Fokus hat, sondern auch wenn eines seiner Kindelemente fokussiert wird. Das ist ideal für Dropdown-Menüs und verschachtelte Navigationen.

Der :target-Selektor adressiert Elemente, die über einen Hash in der URL angesprochen werden, und ermöglicht dadurch URL-basierte Navigation ohne JavaScript. Der relativ neue :has()-Selektor ist besonders mächtig, da er es erlaubt, ein Element basierend auf dem Vorhandensein bestimmter Kindelemente zu stylen, wie z.B. ein „Parent Selector”, nach dem Entwickler jahrelang gefragt haben. Und schließlich bietet das native <details>-Element mit seinem [open]-Attribut eine HTML-eigene Lösung für aufklappbare Bereiche, die sich hervorragend mit CSS stylen lässt.

Jeder dieser Selektoren bildet eine Form von „Zustand” ab, den man direkt im CSS nutzen kann, ohne auf JavaScript Event-Handler oder Zustandsverwaltung angewiesen zu sein. Das macht die Komponenten nicht nur einfacher, sondern auch robuster und in vielen Fällen zugänglicher.

Pattern 1: Akkordeon ohne JavaScript

Ein Klassiker, der heute problemlos ohne JS funktioniert.

HTML

<div class="accordion">
  <input type="checkbox" id="acc-1" hidden>
  <label for="acc-1" class="accordion-header">Titel</label>
  <div class="accordion-panel">Inhalt …</div>
</div>

CSS

.accordion-panel {
  display: none;
}

input:checked + .accordion-header + .accordion-panel {
  display: block;
}

Das Pattern nutzt die natürliche Checkbox-Funktionalität des Browsers und verbindet sie mit dem <label>-Element, das als visueller Trigger fungiert. Über den Adjacent-Sibling-Selektor (+) wird das Panel nur dann sichtbar, wenn die versteckte Checkbox aktiviert ist. Der entscheidende Vorteil dieser Lösung liegt darin, dass sie vollständig ohne JavaScript auskommt und dennoch robust funktioniert.

Die Barrierefreiheit hängt hier stark von der konkreten Umsetzung des Labels ab. Wenn das Label semantisch korrekt mit der Checkbox verknüpft ist und klare Beschriftungen verwendet werden, funktioniert das Pattern auch mit Screenreadern einwandfrei. Für Tastatur-Navigation ist keine zusätzliche Arbeit nötig, da die native Checkbox-Funktionalität bereits vollständig zugänglich ist.

In modernen Projekten bleibt dieses Pattern eine der einfachsten und wartungsfreundlichsten Lösungen für aufklappbare Bereiche. Es funktioniert in allen Browsern, benötigt keine Build-Tools und lässt sich leicht in jedes Design integrieren.

Pattern 2: Dropdowns mit :focus-within

:focus-within reagiert, wenn ein Element oder eines seiner Kinder den Fokus hat.

HTML

<div class="dropdown">
  <button class="trigger">Menü</button>
  <ul class="menu">
    <li><a href="#item1">Item 1</a></li>
    <li><a href="#item2">Item 2</a></li>
  </ul>
</div>

CSS

.menu {
  opacity: 0;
  pointer-events: none;
  transform: translateY(-4px);
  transition: 120ms;
}

.dropdown:focus-within .menu {
  opacity: 1;
  pointer-events: auto;
  transform: translateY(0);
}

Das Dropdown öffnet sich automatisch, sobald der Button oder eines der Menü-Items den Fokus erhält. Der entscheidende Vorteil von :focus-within liegt darin, dass das Menü geöffnet bleibt, solange der Fokus sich innerhalb des Dropdown-Containers bewegt, wie beispielsweise wenn der Nutzer mit der Tab-Taste durch die Menü-Einträge navigiert. Erst wenn der Fokus den Container verlässt, schließt sich das Menü wieder.

Diese Lösung funktioniert vollständig mit der Tastatur und erfüllt damit grundlegende Anforderungen an Barrierefreiheit. Nutzer ohne Maus können durch Tab-Navigation das Menü öffnen und durch die Optionen navigieren. Die pointer-events-Eigenschaft stellt sicher, dass das unsichtbare Menü keine Mausinteraktionen blockiert, wenn es geschlossen ist.

Ein wichtiger Hinweis: Für produktionsreife Dropdowns sollte zusätzlich die Escape-Taste zum Schließen implementiert werden, was dann doch ein kleines JavaScript-Snippet erfordert. Für einfache Hover- und Fokus-Menüs ist die reine CSS-Lösung jedoch vollkommen ausreichend und deutlich wartungsfreundlicher als eine vollständige JavaScript-Implementierung.

Pattern 3: Tabs mit :target

:target reagiert, wenn ein Element über den Hash in der URL angesprochen wird:

.tab-panel {
  display: none;
}

.tab-panel:target {
  display: block;
}

HTML

<nav class="tabs">
  <a href="#tab1">Tab 1</a>
  <a href="#tab2">Tab 2</a>
</nav>

<section id="tab1" class="tab-panel">Inhalt 1</section>
<section id="tab2" class="tab-panel">Inhalt 2</section>

Hinweis: Bei dieser Lösung ist initial kein Tab sichtbar, bis ein Hash in der URL aktiv ist. In der Praxis setzt man häufig einen Default-Hash (z. B. page.html#tab1) oder nutzt :has() auf einem gemeinsamen Container, um einen Fallback zu definieren: .tab-container:not(:has(.tab-panel:target)) .tab-panel:first-of-type { display: block; } – damit wird der erste Tab angezeigt, solange kein :target aktiv ist. Dafür müssen Tabs und Panels in einem gemeinsamen Container liegen.

Die Stärke des :target-Selektors liegt in seiner engen Verknüpfung mit der Browser-URL. Wenn ein Nutzer auf einen Link mit Hash-Fragment klickt (z. B. #tab2), wird das entsprechende Element zum „Target” und kann über CSS spezifisch gestylt werden. Das macht diese Lösung besonders bookmark-freundlich. Der aktive Tab wird Teil der URL und kann direkt verlinkt oder gespeichert werden.

Diese Eigenschaft ist besonders wertvoll für dokumentationsartige Seiten, Tutorial-Inhalte oder FAQ-Bereiche, bei denen Nutzer oft direkt zu einer bestimmten Sektion springen möchten. Die Lösung funktioniert ohne jegliches JavaScript und ist damit extrem robust. Auch die Browser-History funktioniert out-of-the-box. Vor und Zurück navigieren zwischen den Tabs wie gewohnt.

Ein zusätzlicher Vorteil ist die Barrierefreiheit. Screenreader können die Navigation über Hash-Links problemlos nachvollziehen, und die Browser-eigene „Zu Ankerpunkt springen”-Funktionalität bleibt erhalten. Für einfache Tab-Systeme, bei denen keine komplexe Zustandsverwaltung nötig ist, bleibt :target eine elegante und wartungsfreundliche Lösung.

Pattern 4: Validierungen über :has()

:has() ist der mächtigste der modernen Selektoren – besonders bei Formularen.

Beispiel: Fehlerzustände im Formular

.form-group:has(input:invalid) {
  border-color: var(--error);
  background: var(--error-bg);
}

Der :has()-Selektor ermöglicht es, ein Eltern-Element basierend auf dem Zustand seiner Kinder zu stylen. In diesem Beispiel wird die gesamte .form-group mit Fehlerfarben versehen, sobald sie ein ungültiges Input-Element enthält. Das ist besonders elegant, weil keine zusätzlichen CSS-Klassen per JavaScript gesetzt werden müssen.

Die Browser-eigene Formular-Validierung wird hier direkt genutzt. Pseudo-Klassen wie :invalid, :valid, :required oder :in-range existieren schon lange, konnten aber bisher nur auf die Input-Elemente selbst angewendet werden. Mit :has() lassen sich diese Zustände nun auf das gesamte Layout übertragen, ohne dass JavaScript eingreifen muss.

Das führt zu deutlich weniger Code, weniger fehleranfälliger Logik und einer engeren Kopplung zwischen HTML-Validierung und visuellem Feedback. Besonders in Formularen mit vielen Feldern vereinfacht dieser Ansatz die Zustandsverwaltung erheblich.

Hinweis: :has() ist ein relativ neues Feature (Chrome 105+, Safari 15.4+, Firefox 121+). Für Projekte mit Unterstützung älterer Browser sollte ein Fallback-Styling oder eine progressive Enhancement-Strategie eingeplant werden.

Pattern 5: Karten, die sich an Kind-Elementen orientieren

Mit :has() lassen sich Komponenten kontextabhängig stylen.

Beispiel: Card mit Bild

.card {
  display: grid;
  gap: 1rem;
}

.card:has(img) {
  grid-template-columns: 150px 1fr;
  align-items: center;
}

Damit wird die Card automatisch zweispaltig, wenn ein Bild enthalten ist und das völlig unabhängig vom HTML oder Framework. Die Card passt ihr Layout selbstständig an ihren Inhalt an – ohne dass zusätzliche Modifier-Klassen oder Props notwendig sind.

Browser-Unterstützung: :has() ist in modernen Browsern verfügbar (Chrome 105+, Safari 15.4+, Firefox 121+). Für ältere Browser kann ein Fallback-Layout ohne :has() definiert werden, das dann einfach immer einspaltig bleibt.

Pattern 6: Accordion ohne hidden Inputs – dank

Das native <details>-Element ist ein unterschätztes HTML-Feature.

<details class="accordion">
  <summary>Überschrift</summary>
  <p>Inhalt …</p>
</details>

CSS:

.accordion[open] {
  border-color: var(--accent);
}

Das <details>-Element ist ein häufig unterschätztes HTML-Feature, das von Haus aus aufklappbare Bereiche ermöglicht und das ganz ohne CSS oder JavaScript. Das <summary>-Element fungiert als klickbarer Trigger, und der restliche Inhalt wird nur angezeigt, wenn das Element geöffnet ist. Der Browser übernimmt die gesamte Interaktionslogik, einschließlich Tastatur-Navigation, Touch-Gesten und Screenreader-Unterstützung.

Über das [open]-Attribut lässt sich der geöffnete Zustand im CSS adressieren und individuell stylen. Das ist besonders elegant, weil die native Interaktion vollständig erhalten bleibt und lediglich das visuelle Erscheinungsbild angepasst wird. Die Barrierefreiheit ist bei korrekter Verwendung von <details> und <summary> ausgezeichnet, da beide Elemente semantisch klar definiert sind und von assistiven Technologien korrekt interpretiert werden.

Für einfache Akkordeons, FAQ-Bereiche oder aufklappbare Zusatzinformationen ist <details> oft die sauberste Lösung. Die Implementation ist minimal, die Browser-Unterstützung exzellent und das Verhalten funktioniert auch ohne CSS oder JavaScript. Komplexere Anforderungen wie animierte Übergänge erfordern zwar zusätzlichen CSS- oder JS-Code, aber die Basis-Funktionalität ist bereits vollständig vorhanden.

Pattern 7: Toggle ohne JS über :checked

Ein einfaches Pattern für mobile Navigationen:

<input type="checkbox" id="nav-toggle" hidden>
<label for="nav-toggle" aria-label="Menü öffnen/schließen"></label>

<nav class="mobile-nav">

</nav>

CSS:

.mobile-nav {
  transform: translateX(-100%);
  transition: 200ms;
}

#nav-toggle:checked ~ .mobile-nav {
  transform: translateX(0);
}

Auch hier: Kein JavaScript notwendig.

Accessibility-Hinweis: Das Label sollte einen aussagekräftigen aria-label oder sichtbaren Text enthalten, damit Screenreader-Nutzer die Funktion verstehen. Nur das Icon (☰) ist nicht ausreichend barrierefrei. Alternativ kann ein visuell versteckter Text innerhalb des Labels verwendet werden.

Kombination mit Transitionen

Viele dieser Patterns werden erst durch Animationen wirklich elegant.

Beispiel:

.panel {
  max-height: 0;
  overflow: hidden;
  transition: max-height 200ms ease;
}

input:checked ~ .panel {
  max-height: 500px;
}

Dieses Pattern erzeugt flüssige Akkordeonbewegungen und das ganz ohne JavaScript.

Grenzen von CSS State-Logic

So mächtig die modernen CSS-Selektoren auch sind, es gibt klare Grenzen, an denen JavaScript notwendig bleibt. Komplexe Zustandslogik mit mehreren Bedingungen (z. B. „wenn A oder B aktiv ist, aber nicht C”) lässt sich mit CSS-Selektoren kaum noch lesbar abbilden. Auch wenn :has() vieles ermöglicht, bleibt die Komplexität bei verschachtelten Bedingungen schnell unübersichtlich.

Dynamische Daten, die von APIs geladen oder durch Nutzerinteraktionen verändert werden, benötigen ebenfalls JavaScript. CSS kann nur auf bereits im DOM vorhandene Elemente und deren Zustände reagieren, nicht aber neue Inhalte generieren oder externe Daten verarbeiten. Ebenso sind asynchrone Interaktionen wie das Nachladen von Inhalten, Timeouts oder Event-basierte Kommunikation zwischen Komponenten außerhalb des CSS-Bereichs.

Fortgeschrittene UI-Features wie Drag & Drop, Multi-Select mit komplexer Logik oder kontextabhängige Menüs mit dynamischen Inhalten lassen sich mit reinem CSS nicht umsetzen. Auch komplexe Animationen, die auf physikalischen Simulationen basieren oder die mit Nutzer-Gesten interagieren, benötigen JavaScript.

Dennoch deckt CSS bereits erstaunlich viele Standard-Interaktionen sauber ab. Für Akkordeons, Dropdowns, Tabs, einfache Validierungen und kontextabhängiges Styling ist JavaScript oft nicht mehr nötig. Das führt zu wartungsfreundlicheren, performanteren und robusteren Komponenten, solange man die Grenzen kennt und akzeptiert.

Fazit

Moderne CSS-Selektoren haben die Grenze zwischen CSS und JavaScript deutlich verschoben. Interaktionen, die früher zwingend JavaScript erforderten, lassen sich heute oft direkt im Stylesheet abbilden. Das führt zu Komponenten, die in mehrfacher Hinsicht besser werden.

Sie werden einfacher, weil die Zustandslogik direkt im CSS lebt und nicht über Event-Handler und Zustandsverwaltung in JavaScript verteilt ist. Sie werden robuster, weil sie auf Browser-native Funktionen aufbauen, die bereits umfassend getestet sind und in allen modernen Browsern konsistent funktionieren. Die Barrierefreiheit verbessert sich oft automatisch, weil HTML-native Elemente wie <details> oder die Checkbox-Funktionalität bereits vollständig zugänglich sind und von assistiven Technologien korrekt interpretiert werden.

Komponenten werden zudem leichter wiederverwendbar, weil sie keine JavaScript-Abhängigkeiten mitbringen und sich problemlos in verschiedene Projekte und Frameworks integrieren lassen. Die Kontextabhängigkeit steigt durch Selektoren wie :has(), die es ermöglichen, Komponenten basierend auf ihrem Inhalt oder ihrer Umgebung automatisch anzupassen und das ohne dass zusätzliche Klassen oder Props notwendig sind.

Diese Entwicklung bedeutet nicht, dass JavaScript überflüssig wird. Für komplexe Anwendungen bleibt es unverzichtbar. Aber für viele Standard-Interaktionen ist CSS heute die einfachere, wartungsfreundlichere und oft sogar performantere Wahl.


☕ Buy me a coffee

Wenn Dir meine Beiträge gefallen und sie Dir bei Deiner Arbeit helfen, würde ich mich über einen "Kaffee" und ein paar nette Worte von Dir freuen.

Buy me a coffee

Previous Post
Türchen 13 – CSS-Architekturen. BEM, ITCSS, CUBE CSS im Vergleich
Next Post
Türchen 11 – CSS Custom Properties

Kommentare