CSS Container Queries

Warum ich Media Queries immer seltener brauche


Ich entwickle Websites seit 1995. Ich habe HTML-Tabellen-Layouts gebaut, Float-Hacks überlebt und die Einführung von Media Queries gefeiert wie eine Erlösung. Aber seit ich CSS Container Queries in meinen Workflow integriert habe, merke ich: Media Queries lösen oft das falsche Problem.

In diesem Artikel erkläre ich, was Container Queries sind, warum sie einen Paradigmenwechsel bedeuten – und wann ich was einsetze.


Das grundlegende Problem mit Media Queries

Media Queries reagieren auf den Viewport – also die Breite des Browser-Fensters. Das war jahrelang ausreichend, solange Layouts simpel waren. Aber moderne Websites bestehen aus wiederverwendbaren Komponenten: Cards, Hero-Sections, Produktboxen, Navigationen.

Und hier liegt das Problem. Stell dir eine Produktkarte vor. Im Hauptbereich der Seite hat sie 400px Breite. In einer Sidebar vielleicht nur 200px. Der Viewport ist in beiden Fällen gleich gross – dein Layout aber komplett unterschiedlich.

Mit Media Queries musst du dann so etwas schreiben:

/* Globale Breakpoints, die nichts über den Container wissen */
@media (max-width: 768px) {
  .product-card {
    flex-direction: column;
  }
}

Das funktioniert – bis die Karte in einem Grid auftaucht, das selbst responsive ist. Dann stimmen die Breakpoints nicht mehr. Du schreibst Ausnahmen für Ausnahmen. Der CSS-Code wird unübersichtlich, schwer wartbar und eng mit dem Layout verknüpft.


Was sind Container Queries?

Container Queries drehen die Logik um: Statt auf den Viewport zu reagieren, reagiert eine Komponente auf ihren eigenen Container.

Zuerst definierst du, welches Element als Container gilt:

.card-wrapper {
  container-type: inline-size;
  container-name: card;
}

Dann schreibst du Styles, die auf die Breite dieses Containers reagieren – nicht auf den Viewport:

@container card (min-width: 400px) {
  .product-card {
    display: flex;
    flex-direction: row;
  }
}

@container card (max-width: 399px) {
  .product-card {
    flex-direction: column;
  }
}

Die Karte weiss jetzt selbst, wie viel Platz sie hat – völlig unabhängig davon, wo sie auf der Seite platziert wird.


Die drei grössten Vorteile in der Praxis

1. Echte Wiederverwendbarkeit von Komponenten

Das ist für mich persönlich der grösste Gewinn. Eine Komponente, die ich einmal gebaut habe, funktioniert überall – ob in einer Sidebar, in einem Grid oder als Standalone-Element. Ich muss keine kontextabhängigen Klassen mehr vergeben oder CSS-Spezifität missbrauchen.

Früher:

.sidebar .product-card { /* Ausnahme-Styles */ }
.main-content .product-card { /* andere Ausnahme-Styles */ }

Heute: Die Karte regelt das selbst.

2. Weniger JavaScript für Layout-Logik

Vor Container Queries haben viele Entwickler ResizeObserver eingesetzt oder per JavaScript Klassen gesetzt, um auf Grössenänderungen eines Elements zu reagieren. Das ist fehleranfällig, hat Performance-Kosten und gehört eigentlich nicht in JavaScript.

// Das brauche ich nicht mehr
const observer = new ResizeObserver(entries => {
  entries.forEach(entry => {
    if (entry.contentRect.width < 400) {
      entry.target.classList.add('is-narrow');
    }
  });
});

Container Queries erledigen das nativ im Browser – schneller, sauberer, wartbarer.

3. Styles bleiben bei der Komponente

Wer in einem Team arbeitet oder Projekte nach Monaten wieder anfasst, kennt das Problem: CSS-Styles sind über viele Dateien verstreut, globale Breakpoints überschreiben sich gegenseitig und niemand traut sich mehr, etwas zu löschen.

Mit Container Queries schreibst du die Styles direkt in die Komponente. Alles, was die Karte braucht, steht bei der Karte. Das macht Code-Reviews einfacher, Einarbeitung schneller und Refactoring weniger riskant.


Wann Media Queries, wann Container Queries?

Beide haben ihren Platz. Meine Faustregel:

Media Queries nutze ich für das grosse Layout – also für das Grundgerüst der Seite. Wann wechselt die Navigation in ein Hamburger-Menü? Wann wird ein zwei-spaltiges Layout einspaltig? Das sind viewport-abhängige Entscheidungen.

Container Queries nutze ich für Komponenten – alles, was wiederverwendet wird oder an verschiedenen Stellen im Layout auftauchen kann. Cards, Teaser, Widgets, Form-Elemente.

Eine einfache Entscheidungshilfe:

SituationEmpfehlung
Navigation, SeitenstrukturMedia Query
Wiederverwendbare KomponenteContainer Query
Komponente in verschiedenen ContextsContainer Query
Print-StylesMedia Query
Dark ModeMedia Query (prefers-color-scheme)

Bonus: Style Queries – das nächste Level

Container Queries können nicht nur auf Grösse reagieren, sondern demnächst auch auf CSS Custom Properties – sogenannte Style Queries:

@container style(--theme: dark) {
  .card {
    background: #1a1a1a;
    color: white;
  }
}

Das bedeutet: Eine Komponente kann auf den “Zustand” ihres Containers reagieren, ohne dass JavaScript oder zusätzliche Klassen nötig sind. Das ist noch experimentell, aber die Richtung ist klar: CSS wird mächtiger und eigenständiger.


Bonus 2: Der “Has Me”-Selektor – Container Queries ohne Stolperfalle

Container Queries haben eine bekannte Schwachstelle: Der direkte Elternteil einer Komponente muss zwingend mit container-type deklariert sein. Vergisst man das, funktionieren die Container Queries schlicht nicht.

:has(> .card) {
  container-type: inline-size;
}

In der Praxis bedeutet das: Jedes Mal, wenn du eine Komponente an einen neuen Ort im Layout verschiebst, musst du prüfen, ob der neue Elternteil als Container deklariert ist. Das ist fehleranfällig und nervt.

Die Lösung: Die Komponente erklärt ihren Elternteil selbst

Kevin Geary hat eine elegante Lösung gefunden, die er “Has Me Selector” nennt. Die Idee: Statt den Elternteil von aussen zu deklarieren, sagt die Komponente selbst ihrem Elternteil: “Du musst ein Container sein.”

Das funktioniert durch eine Kombination aus :has(), CSS Nesting und dem &-Selektor:

.card {
  :has(> &) {
    container-type: inline-size;
  }
}

Was passiert hier genau? Das & steht in CSS Nesting für den aktuellen Selektor – also für .card. Normalerweise wird & verwendet, um Styles auf das Element selbst oder seine Pseudo-Elemente anzuwenden:

.card {
  &:hover { /* = .card:hover */ }
  &::before { /* = .card::before */ }
}

Aber man kann es auch umkehren. Schreibt man .hero-section &, bedeutet das .hero-section .card – man schreibt also aus dem Inneren der Komponente heraus globales CSS. Das ist der Trick.

:has(> &) bedeutet dann: “Wähle jedes Element, das .card als direktes Kind hat” – also den Elternteil von .card, egal wie er heisst.

Das Ergebnis ist dasselbe wie:

:has(> .card) {
  container-type: inline-size;
}

Der entscheidende Unterschied: Diese Regel wird automatisch aus der Komponente heraus gesetzt. Kein manuelles Nachpflegen, kein Vergessen. Die Karte kümmert sich selbst darum.

Vollständiges Beispiel

So sieht eine robuste, vollständig autonome Komponente mit Container Query aus:

.card {
  /* Hat Me: Elternteil wird automatisch zum Container */
  :has(> &) {
    container-type: inline-size;
  }

  /* Container Query funktioniert jetzt überall */
  @container (min-width: 400px) {
    display: flex;
    flex-direction: row;
    gap: 1.5rem;
  }

  @container (max-width: 399px) {
    flex-direction: column;
  }
}

Diese Karte kann man in jede Sidebar, jedes Grid, jeden Footer setzen – sie funktioniert immer.


Fazit

Container Queries, der “Has Me”-Selektor und Style Queries zeigen alle in dieselbe Richtung: CSS wird eigenständiger, ausdrucksstärker und übernimmt Aufgaben, für die wir früher JavaScript gebraucht haben.

Meine Empfehlung für neue Projekte:

Der Browser-Support aller drei Features ist heute ausgezeichnet. Es gibt keinen guten Grund mehr, sie nicht einzusetzen – dein zukünftiges Ich wird es dir danken.


Marcel Heiniger ist Webentwickler aus Ipsach bei Biel. Seit 1995 baut er Websites – von den ersten HTML-Tabellen bis zu modernen WordPress-Projekten mit API-Integrationen. Mehr auf marcelheiniger.com