CSS moderno: :has y container queries
En esta lección
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.
| Selector | Especificidad |
|---|---|
: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 quebody: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-sizevigila el ancho, que es lo habitual. Existesize(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 essize.
.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:
- Consulta caniuse.com: escribes la característica (por ejemplo, “has”) y te muestra en qué versiones funciona y qué porcentaje de usuarios la soporta.
- 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ística | Disponible en todos los navegadores desde |
|---|---|
:is(), :where() | 2021 |
| Container queries | 2023 |
:has() | 2023 |
| Anidamiento nativo | 2023 |
@layer | 2022 |
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
&__elementode Sass. En CSS nativo,&__titulono crea.tarjeta__titulo:&no se concatena como texto. Esa sintaxis solo funciona en Sass. - Olvidar
container-type. Sin él,@containerno 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
| Novedad | Para qué sirve | Ejemplo |
|---|---|---|
:is() | Agrupar selectores | :is(h1, h2) a |
:where() | Igual, con especificidad 0 | :where(ul, ol) |
:not() | Excluir una lista | li:not(:last-child) |
:has() | Seleccionar según el contenido | form:has(:invalid) |
| Anidamiento | Agrupar reglas de un componente | .boton { &:hover { } } |
container-type + @container | Adaptar según el contenedor | @container (width >= 400px) |
cqi | Unidad relativa al contenedor | font-size: 5cqi |
@layer | Ordenar la cascada por capas | @layer base, componentes; |
@supports | Estilos solo si hay soporte | @supports (display: grid) |
Pruébalo tú
Cambia el código y pulsa Ejecutar (o Ctrl + Enter).
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.