Skip to content

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

txetxu
Published date:
Edit this post

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

Tabla de contenidos

Open Tabla de contenidos

Container Queries: responde 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 ancha dependiendo del diseño del 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>
  <!-- diseño vertical -->
</aside>

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

Unidades de container query

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 de padre que siempre quisimos

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

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

/* Card que contiene una imagen: diseño 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 el body */
body:has(.nav-menu[aria-expanded="true"]) {
  overflow: hidden;
}styles.css

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

Anchor Positioning: tooltips y popovers sin JavaScript

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

/* Declarar el ancla (anchor) */
.btn-trigger {
  anchor-name: --my-button; 
}

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

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

position-try-fallbacks: lógica de colisiones declarativa

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

¿Y el soporte?

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

En 2026, con la distribución actual de navegadores, puedes usar las tres en producción para la mayoría de proyectos. Considera usar polyfills solo si tu audiencia incluye navegadores 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 restricciones, tú declaras el resultado deseado. Las container queries, :has() y el anchor positioning son la culminación de ese paradigma.


Traducido del original (Andrés Ujpán)

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