Zum Inhalt springen

Container Queries: Komponenten, die auf ihren Container reagieren

Baue responsive Komponenten mit CSS Container Queries, die sich am Container statt am Viewport orientieren — ein Layout für Sidebars und Modals.

5 Min. Lesezeit
Nebeneinander dargestellter Vergleich derselben Card-Komponente, einmal klein in einer Seitenleiste und einmal groß in einem Hauptinhaltsbereich, gerendert mit Container Queries

Media Queries beantworten die falsche Frage für komponentenbasiertes Design. Wenn du @media (min-width: 768px) schreibst, fragst du: „Wie breit ist der Viewport?" Aber was deine Komponente tatsächlich wissen muss, ist: „Wie viel Platz habe ich?" Eine Card-Komponente in einer schmalen Seitenleiste und dieselbe Card in einem breiten Inhaltsbereich teilen sich dieselbe Viewport-Breite, brauchen aber völlig unterschiedliche Layouts.

Container Queries lösen dieses Problem, indem Komponenten auf die Abmessungen ihres Containers statt auf den Viewport reagieren. Das macht Komponenten wirklich portabel: Wirf sie in einen beliebigen Layout-Kontext, und sie passen sich automatisch an. Es ist die Responsive-Design-Primitive, die wir uns gewünscht haben, seit wir angefangen haben, Komponentenbibliotheken zu bauen.

Grundlagen der Container Queries

Um Container Queries zu nutzen, definierst du einen Containment-Kontext auf dem Elternelement und schreibst dann Queries gegen diesen Container aus dem Kindelement heraus.

csscss
/* ❌ Media queries: component responds to viewport */
.card {
  display: grid;
  grid-template-columns: 1fr;
}
 
@media (min-width: 600px) {
  .card {
    grid-template-columns: 200px 1fr;
  }
}
/* This card always switches at 600px viewport width
   even if it's in a 300px sidebar */
 
/* ✅ Container queries: component responds to its container */
.card-container {
  container-type: inline-size;
  container-name: card;
}
 
.card {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1rem;
  padding: 1rem;
}
 
@container card (min-width: 400px) {
  .card {
    grid-template-columns: 200px 1fr;
  }
}
 
@container card (min-width: 700px) {
  .card {
    grid-template-columns: 250px 1fr;
    gap: 2rem;
    padding: 2rem;
  }
}
htmlhtml
<!-- Same component, different contexts, different layouts -->
<!-- In sidebar: stays vertical (container < 400px) -->
<aside class="sidebar">
  <div class="card-container">
    <article class="card">
      <img src="thumbnail.jpg" alt="Article thumbnail" />
      <div class="card-content">
        <h3>Article Title</h3>
        <p>Description text here...</p>
      </div>
    </article>
  </div>
</aside>
 
<!-- In main content: goes horizontal (container > 400px) -->
<main class="content">
  <div class="card-container">
    <article class="card">
      <img src="thumbnail.jpg" alt="Article thumbnail" />
      <div class="card-content">
        <h3>Article Title</h3>
        <p>Description text here...</p>
      </div>
    </article>
  </div>
</main>

Container-Query-Einheiten

Container Queries führen neue Einheiten ein, die relativ zu den Abmessungen des Containers sind und Viewport-Einheiten für die komponenteninterne Größenbestimmung ersetzen.

csscss
.card-container {
  container-type: inline-size;
  container-name: card;
}
 
/* Container query units */
.card-title {
  /* cqi = 1% of container's inline size */
  font-size: clamp(1rem, 3cqi, 2rem);
}
 
.card-image {
  /* cqw = 1% of container width */
  /* cqh = 1% of container height */
  /* cqi = 1% of container inline size */
  /* cqb = 1% of container block size */
  /* cqmin = smaller of cqi and cqb */
  /* cqmax = larger of cqi and cqb */
  height: 30cqi;
  object-fit: cover;
}
 
/* Fluid spacing based on container */
.card {
  padding: clamp(0.75rem, 3cqi, 2rem);
  gap: clamp(0.5rem, 2cqi, 1.5rem);
}

Komponentenmuster aus der Praxis

Hier sind praktische Muster, die zeigen, wo Container Queries Probleme lösen, die Media Queries nicht lösen können.

csscss
/* Navigation that adapts to its container */
.nav-container {
  container-type: inline-size;
  container-name: nav;
}
 
.nav-list {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  list-style: none;
  padding: 0;
}
 
.nav-label {
  display: none;
}
 
.nav-icon {
  width: 24px;
  height: 24px;
}
 
/* When nav has enough space, show labels */
@container nav (min-width: 200px) {
  .nav-list {
    gap: 0.5rem;
  }
 
  .nav-label {
    display: inline;
  }
}
 
/* When nav has lots of space, go horizontal */
@container nav (min-width: 600px) {
  .nav-list {
    flex-direction: row;
    gap: 1rem;
  }
}
csscss
/* Data table that adapts its columns to available space */
.table-container {
  container-type: inline-size;
  container-name: data-table;
}
 
.data-table {
  width: 100%;
}
 
/* Hide lower-priority columns when space is tight */
.col-priority-low {
  display: none;
}
 
.col-priority-medium {
  display: none;
}
 
@container data-table (min-width: 500px) {
  .col-priority-medium {
    display: table-cell;
  }
}
 
@container data-table (min-width: 800px) {
  .col-priority-low {
    display: table-cell;
  }
}
 
/* Switch to card layout on very narrow containers */
@container data-table (max-width: 350px) {
  .data-table,
  .data-table thead,
  .data-table tbody,
  .data-table tr,
  .data-table td {
    display: block;
  }
 
  .data-table thead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
  }
 
  .data-table td::before {
    content: attr(data-label);
    font-weight: 600;
    display: block;
    margin-bottom: 0.25rem;
  }
 
  .data-table tr {
    border: 1px solid var(--border-color);
    border-radius: 8px;
    padding: 1rem;
    margin-bottom: 1rem;
  }
}

Container Queries mit Style Queries kombinieren

Style Queries (CSS @container style()) ermöglichen es, den berechneten Stil eines Containers abzufragen, sodass Komponenten ohne JavaScript auf Zustände reagieren können.

csscss
/* Style queries: respond to custom property values */
.card-container {
  container-type: inline-size;
  container-name: card;
}
 
/* Dark variant via custom property */
.card-container[data-theme="dark"] {
  --card-theme: dark;
}
 
@container card style(--card-theme: dark) {
  .card {
    background: #1e293b;
    color: #f1f5f9;
  }
 
  .card-title {
    color: #e2e8f0;
  }
}
 
/* Featured variant */
.card-container[data-featured] {
  --card-featured: true;
}
 
@container card style(--card-featured: true) {
  .card {
    border-left: 4px solid var(--accent-color);
    background: var(--featured-bg);
  }
 
  .card-image {
    aspect-ratio: 16 / 9;
  }
}

Verschachtelte Container

Container können verschachtelt werden, und jede Container Query verweist auf den nächsten Vorfahren mit dem passenden Container-Namen.

csscss
/* Page layout: outer container */
.page-container {
  container-type: inline-size;
  container-name: page;
}
 
/* Card grid: inner container */
.card-grid-container {
  container-type: inline-size;
  container-name: grid;
}
 
/* Individual card: innermost container */
.card-container {
  container-type: inline-size;
  container-name: card;
}
 
/* Grid responds to its container */
.card-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1rem;
}
 
@container grid (min-width: 500px) {
  .card-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}
 
@container grid (min-width: 900px) {
  .card-grid {
    grid-template-columns: repeat(3, 1fr);
  }
}
 
/* Card responds to its own container */
/* (which gets narrower as more columns appear) */
@container card (min-width: 300px) {
  .card {
    grid-template-columns: 120px 1fr;
  }
}

Migrationsstrategie: von Media Queries zu Container Queries

Du musst nicht alles auf einmal umschreiben. Migriere Komponente für Komponente und beginne mit denen, die in mehreren Layout-Kontexten vorkommen.

csscss
/* Step 1: Identify components that need container awareness */
/* Good candidates: cards, navigation, data tables, form layouts */
/* Bad candidates: full-page layouts (viewport is the container) */
 
/* Step 2: Add containment without changing behavior */
.card-wrapper {
  container-type: inline-size;
  /* containment has no visual effect — safe addition */
}
 
/* Step 3: Convert media queries to container queries one at a time */
 
/* Before */
@media (min-width: 768px) {
  .card { grid-template-columns: 200px 1fr; }
}
 
/* After — test in multiple contexts */
@container (min-width: 400px) {
  .card { grid-template-columns: 200px 1fr; }
}
 
/* Step 4: Keep media queries for truly viewport-dependent styles */
/* Page-level layouts, sticky headers, print styles */
@media (min-width: 1024px) {
  .page-layout {
    grid-template-columns: 280px 1fr;
  }
}

Die wichtigsten Erkenntnisse

Container Queries lassen Komponenten auf die Abmessungen ihres Containers statt auf den Viewport reagieren: Eine Card, die bei 400px verfügbarem Platz horizontal wird, funktioniert korrekt, egal ob sie in einer 300px breiten Seitenleiste steckt (bleibt vertikal) oder in einem 800px breiten Inhaltsbereich (wird horizontal) — ganz ohne Codeänderungen. Verwende container-type: inline-size auf Elternelementen und schreibe @container (min-width: ...)-Regeln auf den Kindelementen, kombiniert mit Container-Query-Einheiten wie cqi für fluide Typografie und Abstände, die relativ zum tatsächlichen Platz der Komponente statt zum Bildschirm skalieren. Beginne die Migration mit den Komponenten, die in den unterschiedlichsten Layout-Kontexten vorkommen — Cards, Navigation, Datentabellen — denn diese profitieren am meisten vom Container-Bewusstsein, während Layouts auf Seitenebene, die wirklich von den Viewport-Abmessungen abhängen, bei Media Queries bleiben sollten. Verschachtele Container beim Bau komplexer Layouts, damit jede Ebene unabhängig reagiert: Ein Grid, das seine Spaltenanzahl an den verfügbaren Platz anpasst, enthält Cards, die ihr internes Layout an den Platz jeder einzelnen Card anpassen — so entstehen Kompositionen, die sich auf jeder Ebene anpassen, ohne Breakpoints koordinieren zu müssen.

Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX