Saltar al contenido
css-moderno.css · devschool

CSS moderno: :has y container queries

Lección 20 de 25 · 11 min de lectura · Actualizado el

En esta lección
  1. :is(): agrupar selectores sin repetir
  2. :where(): igual, pero con especificidad cero
  3. :not() con varios selectores
  4. :has(): el selector padre
  5. Anidamiento nativo
  6. Container queries
  7. @layer: ordenar la cascada por capas
  8. Recordatorio: min(), max() y clamp()
  9. Soporte en navegadores
  10. Errores frecuentes
  11. Resumen

CSS ha cambiado muchísimo en los últimos años. Cosas que antes necesitaban JavaScript o un preprocesador como Sass ahora se hacen con CSS puro: seleccionar un elemento según lo que contiene, anidar reglas o adaptar un componente al hueco donde está. En esta lección verás las novedades que ya funcionan en todos los navegadores actuales y que te encontrarás en cualquier proyecto moderno.

Si todavía no dominas los selectores básicos y la especificidad, repasa antes Selectores y Cascada, especificidad y herencia.

:is(): agrupar selectores sin repetir

Imagina que quieres dar el mismo color a los enlaces dentro de la cabecera, del menú y del pie. Sin :is() repites el final del selector tres veces:

header a,
nav a,
footer a {
  color: #2d6cdf;
}

Con :is() escribes la parte común una sola vez:

:is(header, nav, footer) a {
  color: #2d6cdf;
}

La ganancia se nota cuando las combinaciones se multiplican:

/* Todos los títulos dentro de artículos o secciones: 6 x 2 = 12 selectores */
:is(article, section) :is(h1, h2, h3, h4, h5, h6) {
  line-height: 1.2;
}

Otra ventaja: si uno de los selectores de una lista con comas no es válido, el navegador descarta la regla entera. Dentro de :is(), en cambio, los selectores no válidos se ignoran y el resto sigue funcionando. Es una lista “tolerante”.

Especificidad de :is()

:is() toma la especificidad de su argumento más específico. Esto puede sorprenderte:

:is(#principal, p) { color: red; }   /* especificidad (1,0,0), como un id */
p.destacado { color: blue; }          /* (0,1,1) */

Un <p class="destacado"> saldrá rojo, aunque no tenga nada que ver con #principal, porque :is(#principal, p) cuenta como un id entero.

:where(): igual, pero con especificidad cero

:where() funciona exactamente igual que :is(), con una diferencia: su especificidad es siempre 0, pongas lo que pongas dentro.

:where(ul, ol) {
  padding-left: 1.2rem;
}

/* Esta clase gana sin esfuerzo, aunque solo sea (0,1,0) */
.lista-limpia {
  padding-left: 0;
}

¿Para qué quieres una regla con especificidad cero? Para estilos por defecto que cualquiera pueda sobrescribir fácilmente: un reset, una librería de componentes, la tipografía base. Así evitas peleas de especificidad más adelante.

SelectorEspecificidad
:is(#menu, .enlace)(1,0,0): la del id
:where(#menu, .enlace)(0,0,0)
:not(.activo)(0,1,0): la de su argumento
:has(img)(0,0,1): la de su argumento

:not() con varios selectores

:not() selecciona lo que no coincide. Ya lo viste con un solo selector; ahora admite una lista:

/* Todos los botones menos los desactivados y los secundarios */
.boton:not(:disabled, .secundario) {
  background: #2d6cdf;
  color: white;
}

/* Todos los <li> menos el último: separador entre elementos */
.menu li:not(:last-child) {
  border-bottom: 1px solid #ddd;
}

El segundo ejemplo es un patrón muy útil: pones un borde a todos salvo al último, en lugar de ponérselo a todos y quitárselo después.

:has(): el selector padre

Durante años, la pregunta más repetida sobre CSS era: “¿cómo selecciono un elemento según lo que tiene dentro?”. La respuesta era “no se puede”. Hasta que llegó :has().

/* Las tarjetas que contienen una imagen */
.tarjeta:has(img) {
  padding: 0;
}

Lee A:has(B) como “los A que tienen un B dentro”. El elemento que se estiliza es A, no B.

Ejemplos reales

Un formulario que avisa si hay errores. Si algún campo es inválido, el borde del formulario se pone rojo:

form:has(input:invalid) {
  border-left: 4px solid #d33;
}

Resaltar la fila de una tabla con la casilla marcada:

tr:has(input[type="checkbox"]:checked) {
  background: #fff6d6;
}

Un campo con su etiqueta: cambia el color de la etiqueta cuando el campo tiene el foco.

.campo:has(input:focus) label {
  color: #2d6cdf;
  font-weight: bold;
}

Distinto diseño según el número de hijos: si una galería tiene 4 o más fotos, usa más columnas.

.galeria:has(> :nth-child(4)) {
  grid-template-columns: repeat(4, 1fr);
}

:has() también mira hermanos

Con los combinadores + y ~ dentro, :has() selecciona un elemento según lo que viene después:

/* Un h2 que va seguido directamente de un párrafo con clase subtitulo */
h2:has(+ .subtitulo) {
  margin-bottom: 0;
}

En el ejemplo de esta lección tienes una lista de tareas donde cada <li> se tacha cuando su checkbox está marcado, todo sin JavaScript. Pruébalo.

Cuidado: body:has(...) o :root:has(...) obligan al navegador a revisar mucho documento cada vez que algo cambia. Funciona, pero en páginas enormes usa selectores más concretos (.formulario:has(...) mejor que body:has(...)).

Anidamiento nativo

Si has visto Sass, conoces el anidamiento: escribir unas reglas dentro de otras. Ahora CSS lo hace sin herramientas:

.tarjeta {
  padding: 16px;
  border-radius: 8px;

  h2 {
    font-size: 1.2rem;
  }

  p {
    color: #555;
  }
}

Es lo mismo que escribir .tarjeta h2 { ... } y .tarjeta p { ... }, pero todo lo de la tarjeta queda agrupado.

El símbolo &

& representa el selector padre. Lo necesitas cuando quieres pegar algo al padre, sin espacio: pseudoclases, clases extra, etc.

.boton {
  background: #2d6cdf;
  color: white;

  &:hover {                /* .boton:hover */
    background: #1f4fa8;
  }

  &.peligro {              /* .boton.peligro */
    background: #d33;
  }

  .icono + & {             /* .icono + .boton */
    margin-left: 8px;
  }
}

La diferencia entre &:hover y :hover es importante. Sin &, el navegador pone un espacio: .boton :hover significaría “cualquier descendiente de .boton con el ratón encima”.

Media queries anidadas

También puedes meter @media dentro de una regla. Así, todo lo de un componente vive junto:

.menu {
  display: flex;
  flex-direction: column;

  @media (width >= 700px) {
    flex-direction: row;
  }
}

Consejo: no anides más de dos o tres niveles. Cada nivel aumenta la especificidad y hace el CSS más difícil de leer y de sobrescribir.

Container queries

Las media queries preguntan por el tamaño de la ventana. Pero piensa en una tarjeta de producto: puede estar en la columna principal (ancha) o en una barra lateral (estrecha) en la misma pantalla. Con media queries no puedes adaptarla a cada sitio. Con container queries sí: preguntas por el tamaño de su contenedor.

Se hace en dos pasos. Primero, marcas el elemento que hará de contenedor:

.zona-productos {
  container-type: inline-size;   /* vigila su ancho */
}

Después, escribes la consulta con @container:

.producto {
  display: grid;
  gap: 12px;
}

@container (width >= 400px) {
  .producto {
    grid-template-columns: 150px 1fr;   /* imagen a la izquierda */
  }
}

Ahora cada .producto se pone en horizontal cuando su contenedor mide 400px o más, esté donde esté.

Algunas reglas a tener en cuenta:

  • container-type: inline-size vigila el ancho, que es lo habitual. Existe size (ancho y alto), pero exige que el contenedor tenga altura fija.
  • Una container query afecta a los descendientes del contenedor, nunca al contenedor mismo.
  • Si hay contenedores anidados, se usa el más cercano. Para elegir uno concreto, ponle nombre:
.lateral {
  container-type: inline-size;
  container-name: lateral;
  /* atajo: container: lateral / inline-size; */
}

@container lateral (width < 300px) {
  .producto img { display: none; }
}

Las unidades cqi

Dentro de un contenedor puedes usar unidades relativas a su tamaño, igual que vw es relativa a la ventana:

  • 1cqi = 1 % del ancho (inline size) del contenedor.
  • 1cqb = 1 % del alto (block size), si el contenedor es size.
.producto h3 {
  /* el título crece con la tarjeta, no con la ventana */
  font-size: clamp(1rem, 5cqi, 1.6rem);
}

¿Media o container query? Media queries para el esqueleto de la página (cuántas columnas, dónde va el menú). Container queries para componentes que se reutilizan en sitios distintos. Tienes el resto del diseño adaptable en Diseño responsive.

@layer: ordenar la cascada por capas

@layer agrupa reglas en capas y decide qué capa gana, antes de mirar la especificidad:

/* Se declara el orden: la última gana */
@layer reset, base, componentes;

@layer reset {
  * { margin: 0; }
}

@layer componentes {
  .aviso p { margin: 1rem 0; }
}

@layer base {
  #contenido p { margin: 0.5rem 0; }   /* id: muy específico... */
}

Aunque #contenido p tiene un id, pierde contra .aviso p porque la capa componentes va después de base. Esto permite meter CSS de terceros (por ejemplo, Bootstrap) en una capa baja y sobrescribirlo sin pelear con su especificidad. Las reglas que no están en ninguna capa ganan a todas las capas.

Es una herramienta avanzada: te basta con saber que existe y leerla cuando la veas. Verás cómo ordenar un archivo en Organizar el CSS.

Recordatorio: min(), max() y clamp()

Estas funciones no son tan nuevas, pero combinan muy bien con lo anterior:

.contenedor {
  width: min(100% - 32px, 1100px);   /* el menor de los dos */
}

.boton {
  padding: max(8px, 1cqi);           /* el mayor de los dos */
}

h1 {
  font-size: clamp(1.8rem, 1rem + 4vw, 3.5rem);   /* entre un mínimo y un máximo */
}

La primera línea es un truco muy usado: ocupa todo el ancho menos 32px de márgenes, pero nunca más de 1100px. Una línea en lugar de tres. Tienes más en Unidades de medida.

Soporte en navegadores

Todo lo de esta lección funciona en las versiones actuales de Chrome, Edge, Firefox y Safari. Los navegadores se actualizan solos, así que la gran mayoría de usuarios los tiene. Aun así, antes de usar algo nuevo en un proyecto real:

  1. Consulta caniuse.com: escribes la característica (por ejemplo, “has”) y te muestra en qué versiones funciona y qué porcentaje de usuarios la soporta.
  2. Si necesitas una alternativa para navegadores antiguos, usa @supports, que aplica estilos solo si el navegador entiende algo:
.galeria { display: flex; flex-wrap: wrap; }   /* alternativa */

@supports (display: grid) {
  .galeria { display: grid; }
}

@supports selector(:has(a)) {
  /* estilos que dependen de :has() */
}
CaracterísticaDisponible en todos los navegadores desde
:is(), :where()2021
Container queries2023
:has()2023
Anidamiento nativo2023
@layer2022

Errores frecuentes

  • Pensar que :has() estiliza lo de dentro. .tarjeta:has(img) cambia la tarjeta, no la imagen.
  • Olvidar el & en pseudoclases anidadas. .boton { :hover { } } equivale a .boton :hover, con espacio, y no hace lo que esperas.
  • Copiar el &__elemento de Sass. En CSS nativo, &__titulo no crea .tarjeta__titulo: & no se concatena como texto. Esa sintaxis solo funciona en Sass.
  • Olvidar container-type. Sin él, @container no encuentra ningún contenedor y la regla nunca se aplica.
  • Consultar el tamaño del propio contenedor. Una container query solo afecta a sus descendientes. Si quieres cambiar el contenedor, envuélvelo en otro elemento.
  • Usar :is() con ids sin darse cuenta. Toda la regla hereda la especificidad del id. Si quieres algo fácil de sobrescribir, usa :where().

Resumen

NovedadPara qué sirveEjemplo
:is()Agrupar selectores:is(h1, h2) a
:where()Igual, con especificidad 0:where(ul, ol)
:not()Excluir una listali:not(:last-child)
:has()Seleccionar según el contenidoform:has(:invalid)
AnidamientoAgrupar reglas de un componente.boton { &:hover { } }
container-type + @containerAdaptar según el contenedor@container (width >= 400px)
cqiUnidad relativa al contenedorfont-size: 5cqi
@layerOrdenar la cascada por capas@layer base, componentes;
@supportsEstilos solo si hay soporte@supports (display: grid)

Pruébalo tú

Cambia el código y pulsa Ejecutar (o Ctrl + Enter).

css-moderno.html
Resultado

Pon a prueba lo que has aprendido

[CSS] ¿Qué elemento recibe el borde rojo?
form:has(input:invalid) {
  border: 2px solid red;
}

[CSS] Un <p class="nota"> está en la página. ¿De qué color sale?
:where(#principal, p) { color: red; }
.nota { color: blue; }
p { color: green; }

[CSS] ¿Qué selector genera este anidamiento nativo?
.boton {
  &:hover {
    background: navy;
  }
}

[CSS] Esta container query no se aplica nunca. ¿Por qué?
.lateral { width: 500px; }

@container (width >= 400px) {
  .producto { display: flex; }
}

¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.