CSS Container Queries in der Praxis – kein JavaScript mehr
CSS Container Queries in der Praxis: Echte Komponenten, echte Probleme – und warum du für responsive Layouts kein JavaScript mehr brauchst.
Der erste Artikel zu Container Queries hat die Grundlagen erklärt. Dieser hier geht einen Schritt weiter – raus aus dem Theoriebereich, rein in die echten Anwendungsfälle, bei denen Container Queries JavaScript tatsächlich ersetzen.
Denn das ist die große Versprechen: Komponenten, die sich selbst kennen. Keine ResizeObserver-Tricksereien, kein window.innerWidth, kein Zustandsmanagement für etwas, das CSS alleine lösen kann.
Das JavaScript, das wir loswerden wollen
Dieses Muster kennt jeder, der länger mit responsive Komponenten gearbeitet hat:
// Das klassische "ich frage JavaScript, wie groß ich bin"
const card = document.querySelector('.card');
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
const width = entry.contentRect.width;
card.classList.toggle('card--compact', width < 400);
card.classList.toggle('card--wide', width >= 600);
}
});
observer.observe(card);
Das funktioniert – aber es ist JavaScript für ein Problem, das eigentlich CSS lösen sollte. Es gibt einen Observer, der auf Größenänderungen lauscht, der dann Klassen togglet, die dann Styles auslösen. Drei Schichten für etwas, das direkt in der Stylesheet-Ebene passieren könnte.
Derselbe Effekt, nur mit CSS
.card-wrapper {
container-type: inline-size;
}
.card {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
@container (min-width: 400px) {
.card {
grid-template-columns: auto 1fr;
}
}
@container (min-width: 600px) {
.card {
grid-template-columns: 200px 1fr;
gap: 2rem;
}
}
Kein JavaScript. Die Komponente reagiert auf ihren Container – egal ob sie in der Sidebar, im Hauptbereich oder in einem Modal landet.
Praxisbeispiel 1: Produktkarte
Eine Produktkarte, die je nach verfügbarem Platz zwischen drei Layouts wechselt:
Schmal (< 300px): Normal (300–500px): Breit (> 500px):
┌──────────┐ ┌──────────────────┐ ┌───────┬────────────────┐
│ [Bild] │ │ [Bild] │ │ │ Produktname │
│ │ │ │ │ Bild │ ★★★★☆ │
│ Produkt │ │ Produktname │ │ │ 29,99 € │
│ 29,99 € │ │ ★★★★☆ 29,99 € │ │ │ [Kaufen] │
│ [Kaufen] │ │ [Kaufen] │ └───────┴────────────────┘
└──────────┘ └──────────────────┘
.product-card-container {
container-type: inline-size;
container-name: product-card;
}
.product-card {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.product-card__rating {
display: none;
}
/* Ab 300px: Rating anzeigen, Layout anpassen */
@container product-card (min-width: 300px) {
.product-card__rating {
display: flex;
}
.product-card__price,
.product-card__rating {
display: flex;
align-items: center;
gap: 0.5rem;
}
}
/* Ab 500px: Horizontales Layout */
@container product-card (min-width: 500px) {
.product-card {
flex-direction: row;
align-items: flex-start;
}
.product-card__image {
width: 120px;
flex-shrink: 0;
}
}
Die Karte funktioniert jetzt in einem 3-Spalten-Grid genauso wie als einziges Element auf einer Detailseite – ohne eine Zeile JavaScript.
Praxisbeispiel 2: Navigation die sich selbst zusammenfaltet
Ein Navigations-Element, das bei wenig Platz automatisch auf Icons reduziert:
.nav-container {
container-type: inline-size;
container-name: navigation;
}
.nav-item__label {
display: none;
}
.nav-item__icon {
display: block;
}
@container navigation (min-width: 600px) {
.nav-item__label {
display: inline;
}
}
<nav class="nav-container">
<a class="nav-item" href="/blog">
<span class="nav-item__icon">📝</span>
<span class="nav-item__label">Blog</span>
</a>
<a class="nav-item" href="/projekte">
<span class="nav-item__icon">🗂</span>
<span class="nav-item__label">Projekte</span>
</a>
</nav>
Früher hätte man dafür window.resize lauschen oder die Breite der Nav in JavaScript messen müssen. Jetzt passt sich die Navigation selbst an – in jedem Kontext, in dem sie landet.
Container Queries + CSS Custom Properties
Der wirklich spannende Teil: Container Queries lassen sich mit CSS Custom Properties kombinieren. Das öffnet Möglichkeiten, die vorher nur mit JavaScript denkbar waren.
.card-wrapper {
container-type: inline-size;
--card-columns: 1;
--card-image-size: 100%;
}
@container (min-width: 400px) {
.card-wrapper {
--card-columns: 2;
--card-image-size: 180px;
}
}
@container (min-width: 700px) {
.card-wrapper {
--card-columns: 3;
--card-image-size: 220px;
}
}
.card-grid {
display: grid;
grid-template-columns: repeat(var(--card-columns), 1fr);
}
.card__image {
width: var(--card-image-size);
}
Die Custom Properties fließen nach unten durch den DOM – alle Kinder des Containers können sie nutzen. Das Layout-System kommuniziert nach innen, nicht nach außen.
Was Container Queries noch nicht können
Ehrlichkeit gehört dazu: Es gibt Fälle, in denen JavaScript noch gebraucht wird.
Höhe-basierte Queries – container-type: block-size ist in manchen Browsern noch nicht stabil. Für höhenabhängige Layouts ist Vorsicht geboten.
Eltern-abhängige Logik – Wenn eine Komponente wissen muss, in welchem Container sie steckt (nicht nur wie groß), kommt CSS an eine Grenze. container-name hilft dabei, aber komplexe Entscheidungsbäume sind eher JavaScript-Territorium.
Dynamische Inhalte – Wenn die Anzahl der Kindelemente das Layout beeinflusst (z.B. „zeige Sidebar nur wenn mehr als 3 Items vorhanden"), braucht man has() in Kombination – oder doch JavaScript.
Browser-Support: Sicher nutzbar
Container Queries sind seit 2023 in allen modernen Browsern verfügbar:
Chrome 105+ ✅
Firefox 110+ ✅
Safari 16+ ✅
Edge 105+ ✅
Wer noch ältere Browser unterstützen muss, kann mit @supports absichern:
@supports (container-type: inline-size) {
.wrapper {
container-type: inline-size;
}
}
Container Queries sind kein Trend – sie lösen ein echtes, langjähriges Problem. Responsive Komponenten die sich selbst kennen, ohne dass JavaScript als Vermittler einspringen muss. Für alles, was mit Layoutanpassungen zu tun hat, ist das ab jetzt die erste Wahl.