Container Queries: die Komponente, die ihren Container kennt, nicht den Viewport
Container Queries: die Komponente, die ihren Container kennt, nicht den Viewport
Du hast eine Produktkarte. Bild über dem Titel, Preis, Button „In den Warenkorb". Im Haupt-Grid des Shops schaltest du sie ab einer Bildschirmbreite von 768px per Media Query auf ein horizontales Layout um – Bild links, Inhalt rechts, weil das die verfügbare Breite besser ausnutzt. Funktioniert einwandfrei. Dann sieht der Produktverantwortliche dieselbe Karte und will sie in der schmalen Sidebar „Empfohlene Produkte" neben dem eigentlichen Artikeltext haben. Du wirfst die Komponente unverändert hinein – und sie bricht zusammen. Das horizontale Layout, entworfen für 700+ Pixel, versucht sich in eine 260-Pixel-Spalte zu quetschen. Das Bild wird auf einen Streifen zusammengedrückt, der Text bricht in Zeilen mit einzelnen Wörtern um.
Du hast keinen Fehler im CSS gemacht. Deine Media Query funktioniert genau so, wie sie soll – nur beantwortet sie die falsche Frage. @media (min-width: 768px) fragt: „Wie breit ist der Viewport?" Damit die Produktkarte gut aussieht, sollte es ihr aber nie um die Breite des Viewports gehen – sondern darum, wie viel Platz sie von ihrem Elternelement bekommen hat. Das sind zwei völlig verschiedene Fragen, die CSS zwei Jahrzehnte lang wie eine einzige behandelt hat, weil es schlicht keine Möglichkeit gab, die zweite zu stellen.
Warum eine Media Query nicht ausreicht
Eine Media Query ist eine globale Abfrage. Es spielt keine Rolle, ob deine Komponente in einer 1200 Pixel breiten Spalte sitzt oder in einer schmalen Seitenkarte mit 260 Pixeln – @media (min-width: 768px) liefert in beiden Fällen dasselbe true, weil sie das Browserfenster abfragt, nicht das Elternelement der Komponente. Eine Komponente, die ausschließlich auf Media Queries aufbaut, ist also nur in dem Kontext korrekt, für den sie ursprünglich entworfen wurde – und hört auf, korrekt zu sein, sobald derselbe Code an einer anderen Stelle landet. Das untergräbt grundlegend das Versprechen, das Komponenten eigentlich einlösen sollen: dass man sie gefahrlos an beliebiger Stelle im Layout wiederverwenden kann.
Vor 2023 war der einzige praktikable Ausweg ein ResizeObserver in JavaScript – man beobachtet ein Element, misst bei jeder Änderung seine Breite, schreibt das Ergebnis in den State, hängt bedingt eine CSS-Klasse an. Das funktioniert, kostet aber: zusätzlicher Komponenten-State, ein Re-Render bei jeder Größenänderung, Code, den man für jede Komponente einzeln von Hand schreiben muss und der so lange nicht existiert, bis JavaScript ausgeführt wurde – beim serverseitigen Rendering zeigt der erste Frame also ohnehin ein Layout „im Blindflug", bevor der ResizeObserver überhaupt etwas messen konnte.
jsx
// Workaround vor Container Queries – funktioniert, kostet aberimport { useEffect, useRef, useState } from "react";const ProductCard = ({ product }) => {const cardRef = useRef(null);const [isWide, setIsWide] = useState(false);useEffect(() => {const el = cardRef.current;const observer = new ResizeObserver((entries) => {setIsWide(entries[0].contentRect.width > 320);});observer.observe(el);return () => observer.disconnect();}, []);return (<article ref={cardRef} className={`card ${isWide ? "card--wide" : ""}`}>{/* ... */}</article>);};
Container Queries lösen genau dieses Problem ohne eine einzige Zeile JavaScript, ohne State und ohne Re-Render – weil die Frage „wie viel Platz habe ich zur Verfügung" wieder dorthin wandert, wo CSS sie schon immer stellen sollte: ins Stylesheet.
Wie es unter der Haube wirklich funktioniert: Containment, nicht nur neue Syntax
Die Syntax @container sieht aus wie @media mit einem anderen Namen – der Unterschied liegt aber woanders, nämlich darin, was passieren muss, bevor der Browser überhaupt zustimmt, auf eine solche Abfrage zu antworten. Damit ein Element ein „Query-Container" sein kann, musst du das explizit deklarieren:
css
.card-slot {container-type: inline-size;container-name: card;}@container card (min-width: 400px) {.card {grid-template-columns: 40% 1fr;}}
container-type: inline-size ist nicht bloß ein Schalter für den Query-Modus – es ist eine Deklaration von CSS Containment, einer eigenen Spezifikation, die dem Browser mitteilt: „Der Inhalt dieses Elements beeinflusst seine Größe in der Inline-Achse nicht, also kannst du es beim Layouting sicher als unabhängige Insel behandeln." Das ist kein belangloses Implementierungsdetail – es ist die notwendige Voraussetzung dafür, dass Container Queries überhaupt existieren können, ohne sich in einer Endlosschleife zu verfangen. Würde der Browser einem Element gleichzeitig erlauben, seine Größe als Reaktion auf die eigene Breite zu ändern und diese Breite durch seinen Inhalt zu beeinflussen, entstünde eine zirkuläre Abhängigkeit: Der Inhalt ändert die Größe des Containers → die Größenänderung schaltet @container um → die neuen Regeln ändern den Inhalt → der Inhalt ändert erneut die Größe des Containers. Containment durchtrennt diese Schleife an der Wurzel, indem es erzwingt, dass die Größe des Containers in einer bestimmten Achse unabhängig von seinem Inhalt festgelegt wird.
Das hat eine konkrete, praktische Konsequenz bei der Wahl des container-type-Werts:
inline-size– Containment nur in der Inline-Achse (üblicherweise horizontal). Die Höhe des Elements ergibt sich weiterhin frei aus dem Inhalt. Das ist der Wert, zu dem du in 95 % der Fälle greifst – genau wie im Beispiel mit der Produktkarte.size– Containment in beiden Achsen gleichzeitig. Das Element hört komplett auf, sich bei der Größenbestimmung auf seinen Inhalt zu verlassen – das heißt, du musst ihm eine explizite Höhe geben (z. B. perheightoderaspect-ratio), sonst kappt das Containment die natürliche Höhe des Inhalts und die Höhe fällt praktisch auf null.normal– der Standardwert, kein Containment, das Element ist kein Query-Container.
container-name ist optional, aber eine gute Angewohnheit – ohne ihn fragt @container (min-width: 400px) den nächstgelegenen Vorfahren ab, der ein Container ist, egal welcher das gerade ist. In einer flachen Komponente ist das harmlos, aber in einem verschachtelten Layout (Karte innerhalb eines Panels, Panel innerhalb einer Spalte – und sowohl Panel als auch Spalte sind Container) sagt ein benannter Container eindeutig, welchen Vorfahren du konkret abfragst, statt dich auf zufällige Nähe im DOM-Baum zu verlassen.
Praktisches Beispiel: eine Karte, zwei Kontexte, null JavaScript
Zurück zur Produktkarte aus der Einleitung – diesmal so gebaut, dass dieselbe Komponente korrekt sowohl im breiten Grid als auch in der schmalen Sidebar rendert, ohne jegliches Wissen darüber, wo sie gerade eingebettet wurde.
jsx
const ProductCard = ({ product }) => (<div className="product-card-slot"><article className="product-card"><imgsrc={product.image}alt={product.name}className="product-card__image"/><div className="product-card__body"><h3 className="product-card__title">{product.name}</h3><p className="product-card__price">{product.price} €</p><button type="button" className="product-card__cta">In den Warenkorb</button></div></article></div>);export default ProductCard;
css
/* Das Elternelement der Komponente erklärt sich zum Query-Container */.product-card-slot {container-type: inline-size;container-name: product-card;}/* Standardlayout: Bild über dem Inhalt – sicher auf engem Raum */.product-card {display: grid;grid-template-columns: 1fr;gap: 12px;}.product-card__image {width: 100%;aspect-ratio: 4 / 3;object-fit: cover;border-radius: 8px;}/* Ab 360px Breite des CONTAINERS, nicht des Viewports – auf horizontal umschalten */@container product-card (min-width: 360px) {.product-card {grid-template-columns: 40% 1fr;align-items: center;}.product-card__image {aspect-ratio: 1 / 1;height: 100%;}}
Die entscheidende Entscheidung steckt hier in der Struktur, nicht im CSS: container-type sitzt auf .product-card-slot, also dem Elternelement der Karte, nicht auf .product-card selbst. Das ist kein Zufall und kein vorsorglicher Stil – die Spezifikation schließt explizit aus, dass ein Element seine eigene Größe per @container abfragt (wieder dasselbe Problem der zirkulären Abhängigkeit: Ein Element kann nicht gleichzeitig den Container definieren und in Reaktion auf dessen Größe gestylt werden). Deshalb ist ein dünner Wrapper-Container außen und die eigentliche Komponente innen, gestylt innerhalb eines @container-Blocks, das typische, wiederkehrende Muster. Dieselbe CSS-Datei, einmal ins Produkt-Grid geworfen (wo der Slot 480px hat) und einmal in die Sidebar (wo der Slot 240px hat), ergibt zwei unterschiedliche, jeweils korrekte Layouts – ohne Prop, ohne Modifier-Klasse, ohne eine einzige Zeile JavaScript.
Container-Einheiten: Fließend, ohne den Viewport
Container Queries haben ein zweites, weniger bekanntes Stück derselben Spezifikation mitgebracht: relative Einheiten bezogen auf den Container, nicht den Viewport – cqw (1 % der Container-Breite), cqh (1 % der Höhe), cqi und cqb (1 % in der Inline-/Block-Achse, also auch in vertikal geschriebenen Sprachen korrekt), sowie cqmin/cqmax, analog zu vmin/vmax, aber bezogen auf die kleinere oder größere Dimension des Containers.
Ihr naheliegendster Einsatz ist fließende Typografie innerhalb einer Komponente, unabhängig davon, wie breit der Bildschirm des Nutzers gerade ist:
css
.product-card__title {/* Skaliert mit der Breite der Karte, nicht mit der Bildschirmbreite */font-size: clamp(1rem, 0.85rem + 2cqi, 1.375rem);}
Der Unterschied zum bekannten clamp()-Trick mit vw-Einheiten ist subtil, in der Praxis aber fundamental: Ein mit vw skalierter Titel ändert seine Größe zusammen mit der gesamten Seite, also bekommen zwei Kopien derselben Karte – eine im Grid auf voller Breite, eine in der schmalen Sidebar – identische Schriftgröße, weil beide denselben Viewport sehen. Ein mit cqi skalierter Titel reagiert auf den tatsächlichen Platz, den die jeweilige Karte bekommen hat – in der Sidebar wird er kleiner sein, im Grid größer, obwohl der Viewport die ganze Zeit exakt dieselbe Breite hat.
Fallstricke und Best Practices
container-type: sizeohne explizite Höhe frisst den Inhalt. Containment in der Block-Achse kappt das Element von der natürlichen Höhe seiner Nachkommen – gibst du wederheightnochaspect-ratioan, schrumpft der Container faktisch auf null Höhe, und seine Kinder verschwinden optisch, obwohl sie im DOM weiterhin vorhanden sind. In der überwiegenden Mehrheit der Fälle reichtinline-sizeaus, und dieses Problem tritt gar nicht erst auf.- Ein Container kann sich nicht selbst abfragen. Wenn
@containerscheinbar „nicht funktioniert", ist der erste Verdacht genau das:container-typeund der von@containergestylte Selektor sitzen auf demselben Element. Trenne sie –container-typeauf dem Wrapper, die Zielstile auf dem Kind, genau wie im Beispiel der Produktkarte oben. - Container Queries ersetzen Media Queries nicht – sie ergänzen sie. Eine Media Query bleibt das richtige Werkzeug für Entscheidungen auf Seitenebene (z. B. ob überhaupt eine Sidebar angezeigt oder auf ein Hamburger-Menü umgeschaltet wird). Eine Container Query ist zuständig für Entscheidungen auf Komponentenebene, egal wo diese Komponente landet. Ein gutes Layout im Jahr 2026 nutzt in der Regel beide gleichzeitig, jeweils dort, wo die gestellte Frage tatsächlich passt.
- Mach nicht jedes Div „auf Vorrat" zum Container. Containment hat für die Rendering-Engine reale Kosten – es ist ein Signal „behandle mich als isolierte Layout-Einheit", nützlich dort, wo du wirklich eine auf ihre Größe reagierende Komponente hast, nicht als Standardeinstellung für den gesamten DOM-Baum.
- Browser-Unterstützung ist längst kein Problem mehr.
container-typeund@containererreichten Anfang 2023 den Baseline-Status (breit verfügbar) – sie funktionieren nativ in Chrome, Safari und Firefox, ohne Polyfills und ohne@supports. Die Einheitencqw/cqiund der Rest der Familie haben genau dieselbe Abdeckung.
Fazit
Eine wiederverwendbare Komponente ist nicht die, die genug Props und Modifier-Klassen hat, um jeden Ort, an dem sie landen könnte, manuell abzudecken – es ist die, die selbst weiß, wie viel Platz sie hat, und darauf reagiert, ohne fremde Hilfe. Media Queries waren zwei Jahrzehnte lang das einzige Werkzeug von CSS für Responsivität, also haben wir sie zwangsläufig auch dort überstrapaziert, wo es eigentlich um das Elternelement ging, nicht um den Viewport. Container Queries sind keine weitere syntaktische Variante desselben Mechanismus – sie stellen eine andere Frage, gestellt an der richtigen Stelle im DOM-Baum, abgesichert durch Containment, das garantiert, dass die Antwort darauf sich niemals in einer Schleife verfängt. Der Code aus diesem Artikel – eine Komponente, eine CSS-Datei, null JavaScript – funktioniert korrekt im Grid, in der Sidebar und an jedem anderen Ort, von dem du noch nicht weißt, dass du die Komponente dort irgendwann einbaust.