Saltar al contenido

CSS Container Queries: componentes responsivos sin media queries

Cómo las container queries permiten que los componentes respondan al tamaño de su contenedor en lugar del viewport, para piezas reutilizables de verdad.

5 min de lectura
Comparación lado a lado de un componente de tarjeta que adapta su diseño según el ancho del contenedor

Las media queries responden al viewport. Las container queries responden al elemento padre. Esta distinción cambia cómo funcionan los componentes responsivos. Un componente de tarjeta en una barra lateral debería mostrarse de forma distinta a la misma tarjeta en un área de contenido principal — aunque el viewport no haya cambiado.

Las media queries no pueden expresar «si mi padre es estrecho, apila verticalmente». Las container queries sí. Esta es la pieza que faltaba para hacer posibles componentes responsivos verdaderamente reutilizables.

El problema de las media queries en los componentes

Las media queries acoplan el diseño del componente al viewport. Esto funciona cuando cada componente vive en un ancho predecible, pero se rompe cuando los componentes se reutilizan en distintos contenedores.

csscss
/* ❌ Media query — tied to viewport width, not component context */
.product-card {
  display: grid;
  grid-template-columns: 1fr;
}
 
@media (min-width: 768px) {
  .product-card {
    grid-template-columns: 200px 1fr;
  }
}
 
/* Problem: the same card in a 300px sidebar still gets the
   horizontal layout at 768px viewport — it's too wide for the container */
csscss
/* ✅ Container query — responds to the actual available space */
.card-container {
  container-type: inline-size;
  container-name: card;
}
 
.product-card {
  display: grid;
  grid-template-columns: 1fr;
}
 
@container card (min-width: 400px) {
  .product-card {
    grid-template-columns: 200px 1fr;
  }
}
 
/* Now the card switches to horizontal layout only when
   its container gives it 400px+ of space */

La tarjeta en una barra lateral estrecha se mantiene apilada. La misma tarjeta en un área de contenido ancha pasa a disposición horizontal. Sin JavaScript, sin clases condicionales, sin componentes duplicados.

Configurar el containment

Las container queries requieren un contexto de containment — indicarle al navegador qué elemento medir. La propiedad container-type establece este contexto.

csscss
/* Three containment types */
 
/* inline-size: respond to width changes (most common) */
.sidebar {
  container-type: inline-size;
  container-name: sidebar;
}
 
/* size: respond to both width and height changes */
.dashboard-tile {
  container-type: size;
  container-name: tile;
}
 
/* normal: no containment (opt-out, default) */
.unrestricted {
  container-type: normal;
}
 
/* Shorthand: combined name and type */
.panel {
  container: panel / inline-size;
}
htmlhtml
<!-- The container is the parent, the query applies to children -->
<div class="sidebar">           <!-- containment context -->
  <div class="product-card">    <!-- this component adapts -->
    <img src="product.jpg" alt="Product photo" />
    <div class="product-info">
      <h3>Product Name</h3>
      <p>Description text here</p>
    </div>
  </div>
</div>

Un elemento con container-type: inline-size le dice al navegador: «Mis hijos podrían consultar mi ancho. Rastréalo». Sin esta declaración, las consultas @container no tienen ningún contexto contra el cual medir.

Patrones prácticos de componentes

Las container queries destacan en componentes que aparecen en múltiples contextos de diseño.

csscss
/* Navigation component: horizontal in wide containers, vertical in narrow */
.nav-container {
  container: nav / inline-size;
}
 
.nav-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
 
.nav-item {
  padding: 8px 12px;
}
 
.nav-item .label {
  display: none;
}
 
@container nav (min-width: 200px) {
  .nav-item .label {
    display: inline;
  }
}
 
@container nav (min-width: 600px) {
  .nav-list {
    flex-direction: row;
    gap: 8px;
  }
}
csscss
/* Stats card: adapts from compact to full layout */
.stats-container {
  container: stats / inline-size;
}
 
.stat-card {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px;
}
 
.stat-card .chart {
  display: none;
}
 
.stat-card .trend {
  font-size: 12px;
}
 
@container stats (min-width: 300px) {
  .stat-card {
    flex-direction: column;
    align-items: flex-start;
    padding: 16px;
  }
 
  .stat-card .trend {
    font-size: 14px;
  }
}
 
@container stats (min-width: 500px) {
  .stat-card .chart {
    display: block;
    height: 80px;
  }
}

Unidades de container query

Las container queries introducen nuevas unidades CSS relativas a las dimensiones del contenedor. Estas unidades funcionan dentro de bloques @container y en cualquier lugar donde se acepte un valor de longitud.

csscss
.card-wrapper {
  container: card / inline-size;
}
 
.card-title {
  /* cqw = 1% of container's inline size (width) */
  font-size: clamp(14px, 4cqw, 24px);
 
  /* cqh = 1% of container's block size (height) */
  /* cqi = 1% of container's inline size */
  /* cqb = 1% of container's block size */
  /* cqmin = smaller of cqi and cqb */
  /* cqmax = larger of cqi and cqb */
}
 
.card-image {
  /* Image height relative to container width */
  height: 50cqi;
  object-fit: cover;
}
 
@container card (min-width: 400px) {
  .card-title {
    font-size: clamp(16px, 3cqw, 28px);
  }
 
  .card-image {
    height: 30cqi;
  }
}

Las unidades de container query permiten que la tipografía y el espaciado escalen en relación con el contenedor del componente, no con el viewport. Una tarjeta en una columna estrecha obtiene texto proporcionalmente más pequeño sin breakpoints explícitos.

Combinación con diseños de CSS Grid

Las container queries funcionan de forma natural con CSS Grid para crear diseños de dashboard donde cada tile se adapta de manera independiente.

csscss
/* Dashboard grid — each tile is a container */
.dashboard {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 16px;
}
 
.dashboard-tile {
  container: tile / inline-size;
  border: 1px solid var(--border-color);
  border-radius: 8px;
  overflow: hidden;
}
 
/* Tile content adapts to available space */
.tile-header {
  display: flex;
  justify-content: space-between;
  padding: 12px;
}
 
.tile-body {
  padding: 12px;
}
 
.tile-actions {
  display: none;
}
 
@container tile (min-width: 350px) {
  .tile-header {
    padding: 16px 20px;
  }
 
  .tile-body {
    padding: 16px 20px;
  }
 
  .tile-actions {
    display: flex;
    gap: 8px;
    padding: 12px 20px;
    border-top: 1px solid var(--border-color);
  }
}
htmlhtml
<div class="dashboard">
  <!-- Each tile adapts based on how many columns the grid assigns -->
  <div class="dashboard-tile">
    <div class="tile-header">
      <h3>Revenue</h3>
      <span class="badge">+12%</span>
    </div>
    <div class="tile-body">
      <span class="metric">$45,231</span>
    </div>
    <div class="tile-actions">
      <button>Details</button>
      <button>Export</button>
    </div>
  </div>
  <!-- More tiles... -->
</div>

Cuando el grid asigna una columna estrecha a un tile, tile-actions se oculta. Cuando el tile obtiene una columna más ancha, las acciones aparecen. Sin JavaScript, sin resize observers.

Anidar container queries

Los contenedores se pueden anidar. Un hijo consulta a su ancestro más cercano con containment, no al contenedor más externo.

csscss
.page-layout {
  container: page / inline-size;
}
 
.sidebar-panel {
  container: sidebar / inline-size;
}
 
/* This queries the sidebar container, not the page */
@container sidebar (min-width: 250px) {
  .sidebar-widget {
    padding: 16px;
  }
}
 
/* This queries the page container */
@container page (min-width: 1024px) {
  .main-content {
    max-width: 800px;
    margin: 0 auto;
  }
}
 
/* Named containers avoid ambiguity */
/* Without a name, @container queries the nearest ancestor container */
csscss
/* ❌ Ambiguous — which container does this query? */
@container (min-width: 400px) {
  .widget { /* ... */ }
}
 
/* ✅ Explicit — queries the named container */
@container sidebar (min-width: 400px) {
  .widget { /* ... */ }
}

Nombra siempre los contenedores al anidar. Las consultas @container sin nombre coinciden con el contenedor ancestro más cercano, lo que puede producir resultados inesperados cuando la estructura del DOM cambia.

Estrategia de migración

No necesitas reemplazar todas las media queries por container queries. Migra los breakpoints a nivel de componente manteniendo las media queries a nivel de página.

csscss
/* Keep media queries for page layout */
@media (min-width: 768px) {
  .page-layout {
    display: grid;
    grid-template-columns: 250px 1fr;
  }
}
 
@media (min-width: 1200px) {
  .page-layout {
    grid-template-columns: 300px 1fr 250px;
  }
}
 
/* Use container queries for components inside the layout */
.main-content {
  container: main / inline-size;
}
 
.sidebar {
  container: sidebar / inline-size;
}
 
@container main (min-width: 600px) {
  .article-card {
    grid-template-columns: 150px 1fr;
  }
}
 
@container sidebar (min-width: 200px) {
  .sidebar-card .description {
    display: block;
  }
}

Las media queries gestionan el marco de la página. Las container queries gestionan el interior de los componentes. Esta división es limpia y preparada para el futuro.

Puntos clave

  1. Las container queries desacoplan los componentes del viewport — los componentes responden a su espacio realmente disponible
  2. Define container-type: inline-size en los elementos padre — esto establece el contexto de medición
  3. Nombra tus contenedores — evita ambigüedades al anidar múltiples contextos de containment
  4. Usa unidades de container query (cqw, cqi) — para tipografía y espaciado que escalan con el componente
  5. Mantén las media queries para el diseño de página — migra los breakpoints a nivel de componente a container queries
  6. Combínalas con CSS Grid — los grids con auto-fit más container queries crean dashboards totalmente adaptativos
Wilfredo Rujel

Wilfredo Rujel

Ingeniero de Software Full Stack

Compartir esta publicaciónX