Für etliche Probleme, die früher JavaScript-Workarounds erforderten, gibt es heute native CSS-Lösungen.
Allen Punkten dieser Liste ist gemeinsam, dass jede Funktion einen Workaround ersetzt: einen Resize Observer, eine per Skript umgeschaltete Klasse, einen Präprozessor, einen Spezifitäts-Hack. Die meisten werden inzwischen von allen großen Browser-Engines unterstützt. Wo die Unterstützung noch uneinheitlich ist, wird das erwähnt – zusammen mit einem sinnvollen Fallback.
Container Queries
Container Queries haben responsives Design grundlegend verändert. Statt sich ausschließlich auf die Viewport-Abmessungen zu stützen, können sich Komponenten jetzt an die Größe ihres übergeordneten Containers anpassen. Das ermöglicht wirklich wiederverwendbare Komponenten – etwa eine Kartenkomponente, die auf den Platz reagiert, den sie einnimmt, statt auf die Bildschirmbreite.
.card-container {
container-type: inline-size;
}
@container (min-width: 400px) {
.card {
display: grid;
grid-template-columns: 200px 1fr;
}
}
Zwei Details machen Container Queries in der Praxis noch nützlicher.
Benennen Sie Ihre Container. Verschachtelte Container sind in echten Layouts häufig, und eine unbenannte @container-Query greift auf den nächstgelegenen übergeordneten Container. Ein Name beseitigt die Mehrdeutigkeit:
.sidebar {
container: sidebar / inline-size;
}
@container sidebar (min-width: 320px) {
.nav-item { flex-direction: row; }
}
Verwenden Sie Container-Query-Einheiten. cqi entspricht 1 % der Inline-Größe des Containers, sodass font-size: clamp(1rem, 4cqi, 1.5rem) eine Überschrift mit ihrer Karte skaliert statt mit dem Fenster.
Container Style Queries sind der nächste Schritt: Mit @container style(--variant: compact) kann eine Komponente auf eine Custom Property reagieren, die ein Vorfahre gesetzt hat – eine sauberere Alternative zu Modifier-Klassen. Style Queries werden noch nicht überall unterstützt, behalten Sie daher einen Standard ohne Query bei.
Ein Vorbehalt: container-type: inline-size wendet Size Containment auf die Inline-Achse an, sodass das Element seine Breite nicht mehr aus seinem Inhalt ableiten kann. Setzen Sie es auf einen Wrapper, dessen Breite sich aus dem Layout ergibt, etwa eine Grid-Zelle, nicht auf ein Element, das sich an seinen Inhalt anpasst.
Der Selektor :has()
Oft als „Elternselektor“ bezeichnet, ermöglicht :has() Styles, die vom Zustand von Nachfahren oder Geschwistern abhängen. Soll eine Formulargruppe anders aussehen, wenn sie ein ungültiges Eingabefeld enthält? Mit :has() ist das trivial und kommt ohne JavaScript aus.
.field:has(input:user-invalid) {
--field-border: var(--color-danger);
}
.card:has(img) {
grid-template-rows: auto 1fr;
}
/* Scrollen der Seite verhindern, solange ein modaler Dialog geöffnet ist */
html:has(dialog[open]:modal) {
overflow: hidden;
}
Die Pseudoklasse :user-invalid in der ersten Regel sollten Sie gleich mit übernehmen: Anders als :invalid greift sie erst, nachdem der Nutzer mit dem Feld interagiert hat, sodass leere Pflichtfelder beim Laden der Seite nicht als Fehler angezeigt werden.
:has() kann auch zur Seite schauen. h2:has(+ p) trifft auf eine Überschrift zu, auf die unmittelbar ein Absatz folgt – praktisch, um Abstände in Fließtext fein abzustimmen. Verankern Sie Selektoren an einem konkreten Element, statt body:has(...)-Prüfungen gegen das ganze Dokument zu schreiben, sowohl der Lesbarkeit wegen als auch, um dem Browser weniger Invalidierungsarbeit aufzubürden.
Cascade Layers
@layer-Deklarationen geben Entwicklern explizite Kontrolle über die Kaskade und beenden Spezifitätskriege, die Stylesheets seit Jahrzehnten plagen. Organisieren Sie Ihre Styles in Reset-, Basis-, Komponenten- und Utility-Layer, um unabhängig von der Reihenfolge im Quelltext vorhersehbare Ergebnisse zu erzielen.
@layer reset, base, components, utilities;
@layer components {
.button { background: var(--color-primary); }
}
@layer utilities {
.bg-transparent { background: transparent; }
}
Die in der ersten Zeile deklarierte Reihenfolge entscheidet: Eine Regel in utilities schlägt eine Regel in components, selbst wenn der Komponentenselektor spezifischer ist. Spezifität zählt nur zwischen Regeln im selben Layer.
Zwei Verhaltensweisen überraschen viele:
- Styles ohne Layer schlagen alle Styles mit Layer bei normalen Deklarationen. Wenn Sie ein Stylesheet eines Drittanbieters ohne Layer importieren, überschreibt es Ihre Komponenten-Styles in Layern. Verpacken Sie es stattdessen:
@import url("vendor.css") layer(vendor);. !importantkehrt die Layer-Reihenfolge um. Eine wichtige Deklaration in einem frühen Layer schlägt eine in einem späteren Layer, sodass ein Reset eine kritische Regel gezielt schützen kann.
Tailwind CSS v4 basiert auf nativen Cascade Layers – ein gutes Zeichen dafür, dass Layer reif für eine produktive Architektur sind.
Subgrid
Karten-Grids hatten lange einen vertrauten Makel: Unterschiedlich lange Titel schieben Text und Button jeder Karte auf unterschiedliche Höhen. Mit subgrid übernimmt ein untergeordnetes Grid die Spuren seines Eltern-Grids, sodass Elemente in benachbarten Karten auf einer Linie liegen.
.cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
gap: 1.5rem;
}
.card {
display: grid;
grid-row: span 3;
grid-template-rows: subgrid;
}
Jede Karte erstreckt sich über drei Zeilen des Eltern-Grids und gibt diese Zeilen weiter, sodass alle Titel, Texte und Fußbereiche einer Reihe dieselbe Höhe teilen. Subgrid übernimmt außerdem standardmäßig den Abstand (gap) des Eltern-Grids, den Sie am Kindelement überschreiben können.
Farbe: oklch() und color-mix()
oklch() beschreibt Farbe über wahrgenommene Helligkeit, Chroma und Farbton. Der praktische Vorteil gegenüber hsl() besteht darin, dass gleiche Helligkeitswerte tatsächlich gleich hell wirken, sodass sich eine Palette, die durch schrittweise Helligkeitsänderungen entsteht, über alle Farbtöne hinweg vorhersehbar verhält. Zudem lassen sich Farben in größeren Farbräumen wie Display P3 ausdrücken.
:root {
--brand: oklch(62% 0.19 255);
--brand-hover: color-mix(in oklch, var(--brand), black 15%);
--brand-subtle: color-mix(in oklch, var(--brand) 12%, transparent);
}
color-mix() leitet Aufhellungen, Abdunklungen und transparente Varianten von einem einzigen Token ab, sodass ein Theme eine Variable ändern kann statt eines Dutzends. Die relative Farbsyntax geht noch weiter und erlaubt es, einen einzelnen Kanal einer bestehenden Farbe anzupassen: oklch(from var(--brand) calc(l + 0.1) c h) hellt eine Farbe auf, ohne ihren Farbton zu verändern.
Kombinieren Sie dies mit light-dark() und color-scheme: light dark, um die Werte beider Themes in einer Deklaration festzulegen, etwa color: light-dark(#1a1a1a, #f2f2f2).
View Transitions
Die View Transitions API bringt native Seitenübergangsanimationen in Multi-Page-Anwendungen. In Kombination mit der <ClientRouter />-Komponente von Astro erfordern flüssige, App-ähnliche Navigationserlebnisse nur minimalen Code und wirken dabei ausgefeilt.
Es gibt zwei Varianten. Übergänge innerhalb desselben Dokuments, per Skript mit document.startViewTransition() gestartet, nutzen clientseitige Router. Dokumentübergreifende Übergänge brauchen überhaupt kein JavaScript; beide Seiten aktivieren sie per CSS:
@view-transition {
navigation: auto;
}
.post-hero {
view-transition-name: post-hero;
}
Erhalten zusammengehörige Elemente auf der alten und der neuen Seite denselben view-transition-name, lässt der Browser das eine in das andere übergehen. Dokumentübergreifende Übergänge werden noch nicht von jeder Engine unterstützt, doch Browser ohne Unterstützung navigieren einfach normal – damit ist dies eine sichere progressive Verbesserung. Verpacken Sie eigene Übergangsanimationen in @media (prefers-reduced-motion: no-preference), damit Nutzer, die weniger Bewegung wünschen, diese auch bekommen.
Nesting
Natives CSS-Nesting macht Präprozessoren in vielen Projekten überflüssig. Schreiben Sie Styles, die Ihre HTML-Struktur widerspiegeln, ohne auf Sass oder Less zurückzugreifen, und halten Sie Ihre Toolchain schlanker.
.card {
padding: 1.5rem;
& h2 {
margin-block-end: 0.5rem;
}
&:hover {
box-shadow: var(--shadow-md);
}
@media (min-width: 48rem) {
padding: 2rem;
}
}
Es gibt Unterschiede zu Sass. Selektoren lassen sich nicht zusammensetzen, &__title im BEM-Stil funktioniert also nicht. Außerdem wird eine verschachtelte Regel wie :is() behandelt, das die Spezifität seines spezifischsten Arguments übernimmt; Nesting unter einer Selektorliste kann die Spezifität daher auf eine Weise erhöhen, wie es Sass nicht tun würde. Halten Sie die Verschachtelung flach; zwei Ebenen reichen meist völlig aus.
Gültigkeitsbereiche mit @scope
@scope begrenzt eine Gruppe von Regeln auf einen Teilbaum und endet optional an einer inneren Grenze:
@scope (.card) to (.card-content) {
img { border-radius: 0.5rem; }
}
Hier erhalten Bilder innerhalb einer Karte abgerundete Ecken, Bilder im Inhaltsbereich der Karte bleiben jedoch unberührt. Zudem ergänzt @scope die Kaskade um die Nähe des Gültigkeitsbereichs: Wenn zwei Regeln mit Scope bei gleicher Spezifität kollidieren, gewinnt diejenige, deren Scope-Wurzel näher am Element liegt. Die Unterstützung kam in Firefox später als in Chromium und Safari; prüfen Sie daher Ihre Browser-Matrix, bevor Sie sich bei kritischen Dingen darauf verlassen.
Anchor Positioning
Tooltips und Dropdown-Menüs brauchten traditionell eine JavaScript-Bibliothek zur Positionierung. Mit CSS Anchor Positioning kann sich ein Element deklarativ relativ zu einem anderen positionieren:
.trigger {
anchor-name: --menu-trigger;
}
.menu {
position: absolute;
position-anchor: --menu-trigger;
position-area: block-end span-inline-end;
position-try-fallbacks: flip-block;
}
position-try-fallbacks weist den Browser an, das Menü über den Auslöser zu klappen, wenn darunter kein Platz ist – genau der Grenzfall, für den es Positionierungsbibliotheken gibt. Es lässt sich ganz natürlich mit dem Attribut popover kombinieren, um Menüs durch einen Klick daneben zu schließen. Anchor Positioning kam zuerst in Chromium, andere Engines sind erst in jüngerer Zeit gefolgt. Betrachten Sie es daher als Verbesserung: Behalten Sie eine einfache Fallback-Position bei und prüfen Sie die aktuelle Unterstützung, bevor Sie auf eine Bibliothek verzichten.
Kleinere Verbesserungen, die sich lohnen
Eine Handvoll kleinerer Funktionen verdient einen Platz in jedem neuen Stylesheet:
text-wrap: balancegleicht die Zeilenlängen in Überschriften aus und vermeidet ein einzelnes verwaistes Wort in der letzten Zeile.@propertyregistriert eine Custom Property mit einem Typ, sodass sie animiert werden kann, etwa ein Verlaufswinkel.margin-inline,padding-block,inset-inline-startund die übrigen logischen Eigenschaften passen Layouts ohne zusätzliche Regeln an rechtsläufige Sprachen an.:focus-visiblezeigt Fokusringe für Tastaturnutzer an, ohne sie bei jedem Mausklick einzublenden.
Neue Funktionen sicher einführen
Am besten setzen Sie all das schrittweise ein. Prüfen Sie den Baseline-Status einer Funktion, bevor Sie sie voraussetzen, und greifen Sie zu @supports, wenn der Fallback andere Regeln benötigt:
@supports not (anchor-name: --a) {
.menu { inset-block-start: 100%; }
}
Layer, Container Queries, :has(), Nesting und moderne Farbfunktionen sind heute verlässliche Grundlagen. View Transitions, @scope und Anchor Positioning sind hervorragende Verbesserungen. Zusammen ermöglichen sie es CSS, einen großen Teil der Arbeit zu übernehmen, für die früher JavaScript nötig war – mit weniger Code, der ausgeliefert und gewartet werden muss.