Saltar al contenido

Módulos de UI portables con container queries de CSS

Cómo las container queries permiten crear componentes que se adaptan al tamaño de su padre y no al viewport: módulos de UI portables a cualquier layout.

5 min de lectura
Comparación de media queries que apuntan al ancho del viewport frente a container queries que apuntan al ancho del elemento padre para el diseño de componentes responsivos

Las media queries nos enseñaron el diseño responsivo, pero responden a la pregunta equivocada. "¿Qué ancho tiene el viewport?" no ayuda a un componente de tarjeta que se renderiza en una barra lateral, en un área de contenido principal y en un diálogo modal, todo con el mismo ancho de viewport. Las container queries resuelven esto preguntando: "¿Qué ancho tiene mi padre?"

Este cambio de la conciencia global del viewport a la conciencia local del contenedor transforma cómo construimos bibliotecas de componentes. Los componentes se convierten en módulos responsivos autocontenidos que se adaptan a dondequiera que se coloquen.

Los fundamentos de las container queries

Las container queries requieren dos piezas: declarar un contexto de contención en un elemento padre y escribir consultas basadas en tamaño contra ese contenedor.

csscss
/* ❌ Media queries — component doesn't know its context */
.card {
  display: grid;
  grid-template-columns: 1fr;
}
 
@media (min-width: 768px) {
  .card {
    grid-template-columns: 200px 1fr;
  }
}
/* This breaks when .card is in a narrow sidebar at 1024px */
csscss
/* ✅ Container queries — component responds to parent size */
.card-wrapper {
  container-type: inline-size;
  container-name: card;
}
 
.card {
  display: grid;
  grid-template-columns: 1fr;
}
 
@container card (min-width: 400px) {
  .card {
    grid-template-columns: 200px 1fr;
  }
}
 
@container card (min-width: 600px) {
  .card {
    grid-template-columns: 250px 1fr auto;
  }
}

El container-type: inline-size establece la contención de tamaño en el eje inline. También puedes usar size para ambos ejes, pero inline-size es la opción habitual, ya que la mayoría de los layouts solo necesitan responsividad basada en el ancho.

Unidades de container queries

Las container queries introducen nuevas unidades que hacen referencia a las dimensiones del contenedor, de forma similar a como las unidades de viewport hacen referencia al viewport.

csscss
.card-wrapper {
  container-type: inline-size;
}
 
.card-title {
  /* 5% of the container's inline size */
  font-size: clamp(1rem, 5cqi, 2rem);
}
 
.card-image {
  /* Fixed height that scales with container */
  height: 30cqb;
}
 
.card-body {
  /* Padding relative to container width */
  padding: 2cqi;
}

Las unidades son cqw (container query width), cqh (altura), cqi (tamaño inline), cqb (tamaño block), cqmin (dimensión menor) y cqmax (dimensión mayor). Siguen el mismo modelo de propiedades lógicas que margin-inline o padding-block.

Construyendo una tarjeta de producto responsiva

Una tarjeta de producto real necesita manejar al menos tres contextos de layout: estrecho (barra lateral, móvil), mediano (celda de cuadrícula) y ancho (hueco destacado). Las container queries manejan los tres sin saber dónde aparece la tarjeta.

csscss
/* Container setup */
.product-card-container {
  container-type: inline-size;
  container-name: product;
}
 
/* Base: stacked layout for narrow containers */
.product-card {
  display: grid;
  grid-template-areas:
    "image"
    "content"
    "actions";
  gap: 1rem;
  padding: 1rem;
  border-radius: 8px;
  background: var(--surface-1);
}
 
.product-card__image {
  grid-area: image;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: 4px;
  width: 100%;
}
 
.product-card__content {
  grid-area: content;
}
 
.product-card__actions {
  grid-area: actions;
  display: flex;
  gap: 0.5rem;
}
 
/* Medium: side-by-side layout */
@container product (min-width: 480px) {
  .product-card {
    grid-template-areas:
      "image content"
      "image actions";
    grid-template-columns: 200px 1fr;
    grid-template-rows: 1fr auto;
  }
 
  .product-card__image {
    aspect-ratio: 1;
    height: 100%;
  }
 
  .product-card__actions {
    justify-content: flex-end;
  }
}
 
/* Wide: featured layout with more detail */
@container product (min-width: 720px) {
  .product-card {
    grid-template-areas:
      "image content actions";
    grid-template-columns: 280px 1fr auto;
    align-items: center;
  }
 
  .product-card__image {
    aspect-ratio: 4 / 3;
  }
 
  .product-card__actions {
    flex-direction: column;
    align-self: center;
  }
}

Esta única definición de componente funciona en una cuadrícula de dos columnas, un widget de barra lateral, un modal o una sección destacada a ancho completo. Sin JavaScript. Sin cambios responsivos basados en props. El CSS se encarga de todo.

Contenedores anidados y style queries

Los contenedores pueden anidarse, y cada regla @container apunta al contenedor coincidente más cercano a menos que especifiques un nombre. Las style queries amplían este patrón permitiéndote consultar valores de estilo computados en un contenedor.

csscss
/* Nested container setup */
.dashboard-panel {
  container-type: inline-size;
  container-name: panel;
}
 
.widget-slot {
  container-type: inline-size;
  container-name: widget;
}
 
/* Target specific containers by name */
@container panel (min-width: 800px) {
  .widget-slot {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
  }
}
 
@container widget (min-width: 300px) {
  .metric-card {
    flex-direction: row;
    align-items: center;
  }
 
  .metric-card__value {
    font-size: 2rem;
  }
}
 
/* Style queries — respond to custom property values */
@container style(--theme: compact) {
  .metric-card {
    padding: 0.5rem;
    gap: 0.25rem;
  }
 
  .metric-card__label {
    font-size: 0.75rem;
  }
}
 
@container style(--theme: spacious) {
  .metric-card {
    padding: 1.5rem;
    gap: 1rem;
  }
 
  .metric-card__label {
    font-size: 1rem;
  }
}

Las style queries todavía están ganando soporte en los navegadores, pero desbloquean un patrón potente: los componentes padre pueden establecer propiedades personalizadas a las que los componentes hijo responden, sin JavaScript. Un componente de panel establece --theme: compact cuando está en un layout denso, y todos los hijos se ajustan automáticamente.

Integración con frameworks de componentes

Las bibliotecas de componentes modernas se benefician de las container queries como primitiva de estilizado. Así es como integrarlas limpiamente con componentes de React.

tsxtsx
// ❌ JavaScript-based responsive switching
function ProductCard({ layout }: { layout: "narrow" | "wide" }) {
  return (
    <div className={`card card--${layout}`}>
      {/* Parent must know and pass layout variants */}
    </div>
  );
}
tsxtsx
// ✅ Container query-based — component adapts automatically
function ProductCard({
  product,
}: {
  product: { name: string; price: number; image: string };
}) {
  return (
    <div className="product-card-container">
      <article className="product-card">
        <img
          className="product-card__image"
          src={product.image}
          alt={product.name}
          loading="lazy"
        />
        <div className="product-card__content">
          <h3>{product.name}</h3>
          <p className="product-card__price">
            ${product.price.toFixed(2)}
          </p>
        </div>
        <div className="product-card__actions">
          <button type="button">Add to Cart</button>
        </div>
      </article>
    </div>
  );
}
 
// Works everywhere — no layout prop needed
function App() {
  return (
    <>
      <aside className="sidebar">
        <ProductCard product={featured} />
      </aside>
 
      <main className="content-grid">
        {products.map((p) => (
          <ProductCard key={p.id} product={p} />
        ))}
      </main>
    </>
  );
}

La idea clave es que la declaración del contenedor vive en el propio elemento envoltorio del componente. El componente crea su propio contexto de contención, lo que lo hace totalmente portable.

Estrategia práctica de migración

No necesitas reescribir todas las media queries de golpe. Las container queries y las media queries coexisten. La ruta práctica de migración comienza a nivel de componente.

csscss
/* Step 1: Add container context to layout regions */
.main-content { container-type: inline-size; }
.sidebar { container-type: inline-size; }
.modal-body { container-type: inline-size; }
 
/* Step 2: Convert component media queries to container queries */
/* Keep page-level layout media queries as-is */
@media (min-width: 768px) {
  .page-layout {
    grid-template-columns: 300px 1fr;
  }
}
 
/* Convert component-level queries to container queries */
@container (min-width: 400px) {
  .card { grid-template-columns: 150px 1fr; }
}
 
/* Step 3: Use feature detection for progressive enhancement */
@supports (container-type: inline-size) {
  .card-wrapper {
    container-type: inline-size;
  }
 
  @container (min-width: 400px) {
    .card {
      grid-template-columns: 150px 1fr;
    }
  }
}
 
@supports not (container-type: inline-size) {
  @media (min-width: 768px) {
    .card {
      grid-template-columns: 150px 1fr;
    }
  }
}

Conclusiones clave

Las container queries desplazan el diseño responsivo de un enfoque centrado en el viewport a uno centrado en el componente, permitiendo que los componentes se adapten según el espacio que realmente ocupan en lugar del ancho de la ventana del navegador. La declaración container-type: inline-size establece la contención, mientras que las reglas @container consultan contra el contenedor ancestro con nombre o anónimo más cercano. Las unidades de container query (cqi, cqb, cqw, cqh) permiten que los tamaños de fuente, el padding y las dimensiones escalen en relación con el contenedor en lugar del viewport. Anidar contenedores con nombres explícitos te da un control preciso sobre qué ancestro apunta una consulta. Las style queries amplían este modelo permitiendo que los hijos respondan a valores de propiedades personalizadas establecidos por su contenedor, habilitando un cambio de temas sin JavaScript. La ruta de migración es incremental: mantén las media queries para las decisiones de layout a nivel de página y adopta las container queries para la responsividad a nivel de componente, usando @supports para la mejora progresiva en entornos con soporte mixto.

Wilfredo Rujel

Wilfredo Rujel

Ingeniero de Software Full Stack

Compartir esta publicaciónX