Usamos cookies

Utilizamos cookies propias y de terceros para analizar el tráfico y mejorar tu experiencia. Puedes aceptarlas o rechazarlas. Política de privacidad.

Saltar al contenido
—Guías

CSS moderno: container queries, :has(), nesting y capas de cascada

Por
Juan Navarro
Lectura
11 min
Publicado
4 de febrero de 2026

En pocos años CSS ha ganado herramientas que antes pedían JavaScript, preprocesadores o paciencia. Así las usamos en el estudio para construir componentes que se adaptan solos y hojas de estilo que se mantienen con calma.

Durante años, en el estudio teníamos una frase medio en broma cuando un componente no se comportaba: «esto en CSS no se puede». Una tarjeta que debía cambiar de diseño según el hueco que ocupara, un formulario que tenía que marcar en rojo su grupo cuando un campo fallaba, una librería externa que pisaba nuestros estilos. Todo se resolvía con JavaScript, con clases añadidas a mano o con selectores cada vez más largos.

Esa frase ya casi no la decimos. Entre 2022 y 2024 los navegadores incorporaron varias funciones que cambian la manera de escribir CSS: las container queries, el selector :has(), el anidamiento nativo y las capas de cascada. Hoy funcionan en Chrome, Edge, Safari y Firefox, y en nuestros proyectos se han vuelto parte del día a día.

Esta guía es práctica: qué problema resuelve cada una, cómo se escribe, cómo encajan entre sí y cómo introducirlas en una web existente sin sobresaltos. Si te dedicas al diseño y no al código, te servirá igual para entender por qué ahora ciertos componentes se pueden diseñar con más libertad.

01Container queries: componentes que miran su propio espacio

Las media queries preguntan por el tamaño de la ventana. Funcionan muy bien para el layout general, pero fallan en algo muy común: el mismo componente aparece en sitios con anchos muy distintos. Una tarjeta de producto puede vivir en una rejilla de cuatro columnas, en una barra lateral estrecha o a ancho completo en una página de destacados. La ventana es la misma; el espacio de la tarjeta, no.

Las container queries permiten que un componente pregunte por el tamaño de su contenedor. Se hace en dos pasos: declarar qué elemento es un contenedor y escribir reglas que dependan de él.

css
.card-slot {
  container-type: inline-size;
  container-name: card;
}
.card {
  display: grid;
  gap: 1rem;
}
@container card (min-width: 28rem) {
  .card {
    grid-template-columns: 40% 1fr;
    align-items: center;
  }
}
@container card (min-width: 48rem) {
  .card__title {
    font-size: clamp(1.5rem, 4cqi, 2.25rem);
  }
}

container-type: inline-size le dice al navegador que vigile el ancho de ese elemento. A partir de ahí, la tarjeta se coloca en vertical cuando tiene poco espacio y en horizontal cuando le sobra, esté donde esté.

Fíjate en la unidad cqi: es el 1 % del ancho del contenedor. Combinada con clamp(), permite que el título escale con el hueco de la tarjeta y no con la pantalla. Es una de esas piezas pequeñas que dan mucha finura tipográfica.

Cuándo usar cada tipo de consulta

PreguntaHerramientaEjemplo
¿Cómo se organiza la página?Media queryPasar de una a tres columnas en escritorio
¿Cómo se adapta este componente al hueco que tiene?Container queryTarjeta vertical u horizontal
¿Qué prefiere la persona?Media query de preferenciasprefers-reduced-motion, prefers-color-scheme
¿En qué estado está el componente?:has() o clasesFormulario con errores, menú abierto

Una precaución útil: un elemento con container-type no puede tomar su tamaño de su propio contenido en ese eje, así que conviene aplicarlo a un envoltorio que ya tenga el ancho definido por el layout (una celda de la rejilla, una columna), y no a la propia tarjeta.

02:has(), el selector que mira hacia dentro

Durante décadas, CSS solo podía seleccionar elementos mirando hacia arriba o hacia los lados: un párrafo dentro de un artículo, un elemento que va después de otro. No había forma de decir «este contenedor, si contiene tal cosa». Por eso se le llamó durante años el selector padre imposible.

:has() lo hace posible, y abre muchas puertas:

css
/* Tarjeta con imagen: diseño a dos columnas; sin imagen: texto a ancho completo */
.card:has(img) {
  grid-template-columns: 40% 1fr;
}
/* Grupo de formulario con un campo inválido ya tocado */
.field:has(input:user-invalid) {
  --field-color: var(--color-error);
}
/* Bloquear el scroll de la página mientras hay un diálogo abierto */
html:has(dialog[open]) {
  overflow: hidden;
}
/* Navegación: atenuar los enlaces hermanos del que tiene el cursor encima */
.nav:has(a:hover) a:not(:hover) {
  opacity: 0.6;
}

Cada uno de estos ejemplos necesitaba antes una línea de JavaScript que añadiera o quitara una clase. Ahora el estilo se deriva directamente del estado del documento, y eso tiene una ventaja muy concreta: nunca se desincroniza. Si el diálogo se cierra, el scroll vuelve, sin depender de que un script se acuerde de quitar la clase.

El caso del formulario merece un comentario. :user-invalid solo se activa cuando la persona ya ha interactuado con el campo, así que los errores aparecen en el momento oportuno y no en cuanto se carga la página. Es un detalle de experiencia que agradece mucho quien rellena el formulario.

03Nesting nativo: el orden visual del código

El anidamiento era la razón número uno para usar Sass o Less. Ahora CSS lo trae de serie:

css
.button {
  padding: 0.75rem 1.25rem;
  border-radius: var(--radius-m);
  background: var(--color-accent);
  &:hover {
    background: var(--color-accent-strong);
  }
  &:focus-visible {
    outline: 3px solid var(--color-focus);
    outline-offset: 2px;
  }
  .icon {
    inline-size: 1.25em;
  }
  @media (prefers-reduced-motion: no-preference) {
    transition: background 150ms ease;
  }
}

Todo lo que pertenece al botón está en un mismo bloque: sus estados, sus hijos y sus variantes según las preferencias del usuario. Cuando dentro de dos años alguien tenga que cambiar el foco del botón, sabrá exactamente dónde mirar.

Tres pautas para que el nesting ayude

  1. Uno o dos niveles como máximo. Anidar cinco niveles genera selectores largos y muy específicos, difíciles de sobrescribir.
  2. Anida estados y variantes, no la estructura de la página. &:hover, &[aria-expanded="true"] y una media query dentro del componente son buenos candidatos; replicar el árbol HTML completo no lo es.
  3. Una sola forma de anidar por proyecto. Si conviven Sass y CSS nativo, decide cuál manda y aplícalo en todo el código.

Una diferencia con Sass que conviene conocer: el anidamiento nativo no concatena cadenas, así que algo como &__title para generar .card__title (muy habitual en la metodología BEM) no funciona como en Sass. Para eso se escribe el selector completo.

04Capas de cascada: decidir quién gana antes de empezar

La cascada es el mecanismo que decide qué regla se aplica cuando dos chocan. Tradicionalmente dependía de la especificidad del selector y del orden de aparición, y eso producía las famosas guerras de especificidad: un estilo que no se aplicaba, otro selector más largo para ganar, un !important al final para cerrar la discusión.

@layer permite declarar el orden de prioridad por capas, de forma explícita:

css
@layer reset, base, tokens, components, utilities, overrides;
@layer reset {
  *, *::before, *::after { box-sizing: border-box; }
}
@layer components {
  .card { padding: var(--space-4); }
}
@layer utilities {
  .p-0 { padding: 0; }
}
@import url("slider-externo.css") layer(vendor);

Las capas declaradas más tarde ganan a las anteriores, sin importar la especificidad de sus selectores. Así, una utilidad simple como .p-0 en la capa utilities gana a .page .sidebar .card de la capa components, porque su capa va después. Y los estilos que no están en ninguna capa ganan a todas las capas, cosa útil para correcciones puntuales.

El último ejemplo es donde más se nota en proyectos reales: al importar una librería externa (un carrusel, un selector de fechas, un widget de reservas) dentro de una capa propia, sus estilos dejan de competir con los tuyos de forma impredecible. Si declaras la capa vendor al principio de la lista, todo tu código gana a la librería de forma natural.

05Cómo encajan las cuatro en un sistema de componentes

Por separado, cada función es útil. Juntas cambian la forma de pensar un sistema de diseño. En nuestros proyectos la estructura suele ser esta:

  • Capas para el orden general: reset, base tipográfica, tokens (colores, espacios, radios como custom properties), componentes, utilidades.
  • Nesting dentro de cada componente, para que sus estados y variantes vivan juntos.
  • Container queries para que cada componente decida su composición según el hueco, de modo que el mismo bloque sirve en la portada, en un listado y en un lateral.
  • `:has()` para los estados que dependen del contenido: con imagen o sin imagen, con error o sin él, con menú abierto o cerrado.

El resultado se parece mucho a cómo se diseña en Figma con componentes y variantes, y por eso la conversación entre diseño y desarrollo es más fluida. Quien diseña puede pensar en una tarjeta que funciona a 280 o a 720 píxeles de ancho, y quien desarrolla la construye exactamente así. Encaja de forma natural con el enfoque de diseño atómico, donde las piezas pequeñas se combinan en piezas mayores. Y complementa, sin sustituirlo, el planteamiento mobile first: se sigue diseñando desde lo pequeño, solo que ahora lo pequeño puede ser el hueco del componente y no solo la pantalla.

Para que esto funcione desde el diseño, pedimos en los archivos de Figma que cada componente se muestre al menos a tres anchos de contenedor (estrecho, medio y amplio) en lugar de a tres tamaños de pantalla. Es un cambio pequeño en la entrega que ahorra muchas preguntas después.

06Adoptarlas en una web existente, paso a paso

No hace falta reescribir la hoja de estilos de golpe. Este es el orden que seguimos cuando modernizamos el CSS de una web en marcha:

  1. Comprueba el público. En la analítica, revisa qué navegadores y versiones usan tus visitantes. En la mayoría de webs de negocio, la inmensa mayoría ya tiene navegadores actualizados.
  2. Envuelve lo que ya tienes en capas. Mete el CSS existente en una capa legacy y escribe lo nuevo en capas posteriores. Lo nuevo ganará sin necesidad de selectores más largos.
  3. Empieza por un componente con problemas. La tarjeta que se rompe en el lateral es la candidata perfecta para la primera container query.
  4. Sustituye clases de estado por `:has()` donde aporte. Cada script que solo añadía una clase para dar estilo es una oportunidad.
  5. Aplica mejora progresiva. Si un componente usa algo reciente, el estilo base debería verse bien sin ello. Con @supports puedes condicionar la mejora: @supports selector(:has(a)) o @supports (container-type: inline-size).
  6. Prueba en dispositivos reales. Un iPhone con un par de años y un Android de gama media dicen más que cualquier emulador.

07Menos código, más diseño

Lo que más nos gusta de este CSS moderno no es la técnica en sí, sino lo que permite: menos JavaScript que mantener, componentes que se comportan bien en cualquier sitio y una hoja de estilos que se puede leer de arriba abajo. Eso se traduce en webs más ligeras y en diseños que llegan a producción tal y como se pensaron.

En SIMA lo aplicamos en todos los proyectos de desarrollo web, donde diseño y código avanzan de la mano desde el primer componente. Si estás pensando en renovar la base técnica de tu web o quieres que revisemos cómo está construida, escríbenos y lo vemos juntos.

Juan Navarro

Juan Navarro

Fundador y director creativo de SIMA, en Estepona. Más de 25 años dando forma a marcas, webs y espacios.

—FAQ

Preguntas
al margen.

Las dudas que suelen aparecer después de leer esta guía.

Hacer una pregunta

No, se complementan. Las media queries siguen siendo la herramienta para decisiones que dependen de la ventana o del dispositivo, como el layout general de la página, la orientación o las preferencias del usuario (modo oscuro, movimiento reducido). Las container queries se encargan de los componentes, que se adaptan al hueco que les toca dentro de esa página.

Sí. Ambas funcionan en las versiones actuales de Chrome, Edge, Safari y Firefox, y forman parte de Baseline, la referencia de compatibilidad que publican los equipos de los navegadores en web.dev. Si tu público incluye dispositivos muy antiguos, lo sensato es que el estilo base funcione bien sin ellas y que la mejora se aplique encima.

Para muchos proyectos ya no es imprescindible: el anidamiento, las variables (custom properties) y funciones como clamp() cubren lo que antes justificaba un preprocesador. Sass sigue aportando mixins, bucles y funciones propias, así que en sistemas grandes puede tener sentido. Lo importante es no mezclar dos formas de anidar en el mismo proyecto.

Baseline es una etiqueta que indica que una función web está disponible en las versiones actuales de los principales navegadores. «Newly available» significa que acaba de llegar a todos; «Widely available» que lleva al menos treinta meses en ellos. La verás en MDN y en web.dev junto a cada propiedad, y es la forma más rápida de decidir.

En la práctica, no de forma apreciable. @layer cambia el orden en que se resuelven los conflictos entre reglas, no la cantidad de trabajo que hace el navegador. Su beneficio está en el mantenimiento: menos !important, menos especificidad artificial y menos sorpresas al integrar estilos de terceros.

Del papel al proyecto¿Lo hacemos realidad?