Anidamiento nativo: la sintaxis y cómo lo procesa el motor
Qué es exactamente una regla anidada, por qué el orden entre declaraciones y reglas hijas importa, dónde se puede anidar y qué significa que todo esto ocurra en el navegador y no en el build.
El anidamiento llevaba una década en la lista de peticiones y llegó tarde a propósito. Lo que lo volvió urgente no fue la comodidad de escribir menos, ni el deseo de imitar a Sass: fueron las reglas condicionales. Cuando una consulta de contenedor tiene que ir pegada al componente que consulta, y una capa tiene que envolver a un puñado de reglas relacionadas, escribir todo eso en el nivel superior deja de ser incómodo y pasa a ser inmanejable.
- Escribir reglas anidadas con la sintaxis correcta y saber qué produce cada una.
- Predecir el resultado cuando hay declaraciones antes y después de una regla anidada.
- Enumerar dónde se puede anidar y dónde no.
- Explicar qué implica que el anidamiento se resuelva en el navegador y no en la compilación.
Una regla dentro de otra
Una regla de estilo puede contener otras reglas de estilo. El bloque interior se interpreta en relación con el selector exterior.
.card {
border: 1px solid;
border-radius: 0.75rem;
& > img { aspect-ratio: 16 / 9; }
&:hover { border-color: currentColor; }
& .titulo { font-weight: 600; }
}
Eso equivale exactamente a estas cuatro reglas escritas por separado:
.card { border: 1px solid; border-radius: 0.75rem; }
.card > img { aspect-ratio: 16 / 9; }
.card:hover { border-color: currentColor; }
.card .titulo { font-weight: 600; }
El símbolo & se llama selector de anidamiento y representa al selector exterior. Aparece donde tú lo pongas y tantas veces como quieras, incluido en el medio o al final:
.tema-oscuro {
/* El exterior va delante: .tema-oscuro .boton */
& .boton { --fondo: black; }
}
.boton {
/* El exterior va detras: .tema-oscuro .boton, escrito desde el otro lado */
.tema-oscuro & { --fondo: black; }
/* Compuesto con otra clase: .boton.grande */
&.grande { padding-block: 0.75rem; }
}
Fíjate en la diferencia entre & .grande y &.grande. Con espacio es un descendiente; sin espacio es un compuesto sobre el mismo elemento. Es el mismo espacio que separa .a .b de .a.b en CSS normal, y el mismo despiste que provoca desde 1996.
El orden importa
Un bloque anidado puede mezclar declaraciones y reglas hijas, y las declaraciones se aplican en el orden en que aparecen, no antes de todo.
.boton {
color: blue;
&:hover { color: green; }
/* Esta declaracion va DESPUES de la regla anidada. */
color: red;
}
El color base es rojo, porque la segunda declaración de color gana por orden de aparición dentro del mismo origen y la misma especificidad. Ese comportamiento parece obvio y no lo era: las primeras implementaciones elevaban todas las declaraciones por encima de las reglas anidadas, con lo que el ejemplo anterior daba azul. La especificación se corrigió y los motores se alinearon después, así que hoy el orden de escritura es el orden de aplicación.
De ahí sale una regla de higiene que te ahorra depender de en qué versión se arregló cada motor: pon todas las declaraciones primero y las reglas anidadas después. No pierdes nada, se lee mejor, y el resultado es idéntico en cualquier implementación.
Un selector con comas dentro de un bloque anidado se comporta como una unidad agrupada, no como reglas independientes. .card { & h2, & h3 { ... } } produce :is(.card) :is(h2, h3), y eso tiene consecuencias de especificidad que verás en dos lecciones. La versión sin sorpresas es agrupar tú mismo: & :is(h2, h3).
Dónde se puede anidar
Dentro de una regla de estilo puedes poner otras reglas de estilo y también reglas condicionales. Esto último es lo que justifica la característica entera:
.card {
display: grid;
gap: 1rem;
@media (min-width: 48rem) {
grid-template-columns: 12rem 1fr;
}
@container (min-inline-size: 30rem) {
padding: 2rem;
}
@supports (corner-shape: squircle) {
border-radius: 1.5rem;
}
}
Antes del anidamiento, esas tres condiciones tenían que vivir lejos del componente, cada una en su bloque, repitiendo el selector. Con el componente y sus condiciones juntos, mover el componente de sitio deja de ser una operación arqueológica.
Las reglas condicionales anidadas se comportan como si envolvieran una regla con el selector exterior implícito. Es decir, el bloque de arriba equivale a @media (min-width: 48rem) { .card { grid-template-columns: 12rem 1fr; } }.
Dónde no se puede anidar: dentro de @keyframes, cuyos bloques solo admiten declaraciones, y dentro de @font-face, @property o @counter-style, que son descriptores y no reglas de estilo. Y un apunte histórico por si te lo encuentras en material antiguo: los primeros borradores incluían una regla @nest para los casos en que el & no iba al principio. Se eliminó cuando se relajó la sintaxis; si ves @nest en un artículo, el artículo es anterior a 2023.
Esto ocurre en el navegador
La diferencia más importante con un preprocesador no es sintáctica: es cuándo pasa. Sass resuelve el anidamiento en tu máquina y entrega al navegador un archivo plano donde no queda rastro de la estructura. El anidamiento nativo lo resuelve el motor, y la estructura sobrevive hasta el CSSOM.
const hoja = document.styleSheets[0];
const regla = hoja.cssRules[0]; // la regla .card
console.log(regla.cssRules.length); // las reglas anidadas dentro de ella
Eso tiene tres consecuencias prácticas. En las herramientas de desarrollo ves la regla anidada tal como la escribiste, con su contexto, en lugar del selector expandido que te devolvía un mapa de fuentes. Cualquier código que recorra hojas de estilo —un extractor de CSS crítico, una herramienta de análisis, un tematizador en tiempo de ejecución— tiene que estar preparado para recursión, porque una regla de estilo ya no es una hoja del árbol. Y el archivo que envías por la red es más pequeño, porque el selector exterior aparece una vez y no una vez por regla hija; con gzip la diferencia se estrecha, pero no desaparece.
Y hay una consecuencia menos obvia y más profunda: como el motor conoce la relación padre-hijo entre reglas, & no es una sustitución de texto sino una referencia al selector exterior, evaluada con las reglas del lenguaje. Eso es lo que produce las diferencias con Sass que verás en esta misma serie, y explica por qué algunas cosas que funcionaban en el preprocesador no funcionan aquí.
Durante años, la petición de anidamiento nativo se despachaba con un argumento razonable: es azúcar sintáctico, ya existen preprocesadores, y meter azúcar en la plataforma tiene un coste permanente que hay que justificar. El argumento era correcto mientras CSS no tuvo reglas condicionales dependientes del contexto. Lo que cambió el cálculo fue @container. Una consulta de contenedor es, por definición, una condición sobre el entorno inmediato de un componente concreto, y no tiene ningún sentido escribirla lejos de ese componente: no existe un lugar central donde agrupar “todas las consultas de contenedor” como sí existía para agrupar los puntos de ruptura de una hoja responsive, porque cada consulta habla de un contenedor distinto. Lo mismo pasa con @scope, cuyo bloque solo tiene sentido leído junto a la raíz que declara. Sin anidamiento, adoptar estas dos características obligaba a repetir el selector del componente en cada condición y a esparcir su definición por el archivo, que es exactamente lo contrario de lo que ambas prometían. Por eso el anidamiento nativo no es la respuesta tardía a una petición de comodidad, sino una pieza de infraestructura que las características de los últimos cinco años necesitaban para ser usables. Y por eso el criterio para decidir si anidar algo no debería ser “queda más bonito”, sino “esta regla solo tiene sentido leída junto a su padre”. Con ese criterio, anidar condiciones es casi siempre correcto y anidar estructura casi siempre no lo es.
- Expande a mano a reglas planas un bloque anidado de tres niveles con
&en posiciones distintas. - Escribe el caso donde
& .xy&.xproducen resultados diferentes y compruébalo. - Coloca una declaración después de una regla anidada y verifica en el inspector cuál gana.
- Anida
@media,@containery@supportsdentro de un mismo componente y comprueba el resultado en el CSSOM. - Recorre
document.styleSheetscon un script que cuente reglas y arréglalo para que baje por las anidadas.