Skip to content

CSS moderno en 2026: container queries, :has() y anchor positioning

Andrés Ujpán

Durante años, la respuesta a “¿cómo hago X en CSS?” era “usa JavaScript”. En 2026 esa respuesta ya no aplica para la mayoría de los casos. Tres features en particular redibujaron el mapa del diseño en el navegador.

Table of contents

Open Table of contents

Container Queries: responder al contenedor, no a la pantalla

Las media queries responden al ancho del viewport. El problema: un componente puede vivir en una columna estrecha o en una amplia dependiendo del layout padre. Las container queries resuelven esto.

/* Declarar el contenedor */
.card-wrapper {
  container-type: inline-size; 
  container-name: card;
}

/* El componente responde a su contenedor */
@container card (min-width: 400px) {
  .card {
    display: grid;
    grid-template-columns: 200px 1fr;
  }

  .card__image {
    grid-row: 1 / 3;
  }
}

@container card (max-width: 399px) {
  .card {
    display: flex;
    flex-direction: column;
  }
}card.css
<!-- El mismo componente funciona en cualquier contexto -->
<aside class="card-wrapper" style="width: 300px">
  <article class="card">...</article>
  <!-- layout vertical -->
</aside>

<main class="card-wrapper" style="width: 700px">
  <article class="card">...</article>
  <!-- layout horizontal -->
</main>card.html

Container query units

Las queries también exponen unidades relativas al contenedor:

.card__title {
  font-size: clamp(1rem, 4cqi, 2rem); /* cqi = container query inline size */
}typography.css

La pseudoclase :has() — el selector padre que siempre quisimos

:has() selecciona un elemento en función de sus descendientes. Es el “selector de padre” que el CSS negó durante décadas.

/* Formulario con campo requerido vacío */
form:has(input:required:invalid) .submit-btn {
  opacity: 0.5;
  pointer-events: none;
}

/* Card que contiene imagen: layout diferente */
.card:has(img) {
  display: grid;
  grid-template-columns: 150px 1fr;
}

.card:not(:has(img)) {
  padding: 1.5rem;
}

/* Navbar con menú abierto: deshabilitar scroll en body */
body:has(.nav-menu[aria-expanded="true"]) {
  overflow: hidden;
}styles.css

:has() tiene soporte en todos los browsers modernos desde 2023. Puedes usarlo hoy en producción sin polyfills.

Anchor Positioning: tooltips y popovers sin JavaScript

Antes de Anchor Positioning, colocar un tooltip relativo a su trigger requería calcular posiciones con JavaScript. Ya no:

/* Declarar el ancla */
.btn-trigger {
  anchor-name: --mi-boton; 
}

/* Posicionar el tooltip relativo al ancla */
.tooltip {
  position: absolute;
  position-anchor: --mi-boton; 
  bottom: calc(anchor(top) + 8px); 
  left: anchor(center); 
  transform: translateX(-50%);

  /* Voltear automáticamente si no cabe */
  position-try-fallbacks: flip-block; 
}tooltip.css
<button class="btn-trigger" popovertarget="tip">Hover me</button>
<div id="tip" class="tooltip" popover>
  Este tooltip se posiciona solo, sin JS.
</div>tooltip.html

position-try-fallbacks: lógica de colisión declarativa

.tooltip {
  position-try-fallbacks:
    flip-block,
    /* prueba arriba si no cabe abajo */ flip-inline,
    /* prueba izquierda si no cabe derecha */ flip-start; /* combina ambos */
}tooltip.css

¿Y el soporte?

FeatureChromeFirefoxSafari
Container Queries105+ ✓110+ ✓16+ ✓
:has()105+ ✓121+ ✓15.4+ ✓
Anchor Positioning125+ ✓131+ ✓18+ ✓

En 2026, con la distribución actual de browsers, puedes usar las tres en producción para la mayoría de proyectos. Considera polyfills sólo si tu audiencia incluye browsers muy antiguos.

El CSS de hoy es declarativo y expresivo

La revolución silenciosa del CSS no fue Grid ni Flexbox. Fue el cambio de mentalidad: el navegador razona sobre las restricciones, tú declaras el resultado deseado. Container queries, :has() y anchor positioning son la culminación de ese paradigma.

Editar este post
Anterior
Docker Compose en 2026: buenas prácticas que sí importan
Siguiente
React 19: useActionState, useOptimistic y el fin de los estados de carga manuales