wandres.dev
ANIDAMIENTO NATIVO · Nesting sin preprocesador

Cuándo no anidar: el coste de la profundidad

Qué se paga al anidar estructura en lugar de condiciones, por qué la regla de los tres niveles es la respuesta correcta a la pregunta equivocada, y un estilo de casa que aguanta.

⏱ 15 min

La guía de estilo clásica de Sass decía “no anides más de tres niveles” y funcionaba razonablemente bien como heurística. Pero la profundidad es un síntoma, no la enfermedad: hay bloques de cinco niveles perfectamente sanos y bloques de dos que van a romperse en el próximo cambio de plantilla. Lo que distingue a unos de otros no es cuánto anidan, sino qué anidan.

🎯 Al terminar esta lección sabrás
  • Enumerar los cuatro costes concretos de anidar estructura.
  • Distinguir una dependencia intrínseca de una extrínseca al decidir si anidar.
  • Aplicar un criterio que sustituya con ventaja a la regla de los tres niveles.
  • Escribir un bloque de componente con un orden interno defendible.

Lo que se paga

Cuatro costes, en orden creciente de gravedad.

Especificidad acumulada. Cada nivel añade un compuesto y el resuelto pesa la suma. No es dramático mientras el padre sea homogéneo, pero un bloque de cuatro niveles produce reglas de (0,4,0) que solo se sobrescriben con otras cuatro clases, y eso empuja a todo el mundo hacia el !important.

Coste de evaluación. El selector resuelto es más largo y contiene un :is() por nivel. Cada uno es barato; la suma sobre miles de reglas y millones de comprobaciones deja de serlo. No es la razón principal para no anidar, pero tampoco es cero.

Ilegibilidad local. Una regla anidada a cuatro niveles no se puede entender leyéndola: hay que subir hasta la raíz del bloque reconstruyendo el contexto. Las herramientas de desarrollo te enseñan el selector resuelto y mitigan el problema en el navegador, pero no en el editor, que es donde se escribe el código.

Acoplamiento con el DOM. El grave. Una regla anidada a cuatro niveles afirma cuatro hechos estructurales sobre el HTML. Cuando alguien mete un envoltorio nuevo, quita un contenedor o mueve un elemento un nivel arriba, la regla deja de aplicar. Sin error, sin aviso, sin nada en la consola: simplemente el estilo desaparece y el fallo se descubre en producción, en una pantalla concreta, semanas después.

Intrínseco frente a extrínseco

Aquí está el criterio que sustituye a la regla de los tres niveles. Cuando anidas algo, estás declarando que esa regla no tiene sentido sin el padre. La pregunta correcta es si esa afirmación es estable en el tiempo.

Hay dependencias intrínsecas: las que forman parte de la definición del componente y no pueden cambiar sin que cambie el componente. El estado de hover del botón es del botón. La consulta de contenedor que reorganiza la tarjeta es de la tarjeta. El pseudo-elemento decorativo pertenece al elemento que lo genera. Ninguna de esas cosas se va a mover a otro sitio en el próximo sprint, porque no existen fuera de ahí.

Y hay dependencias extrínsecas: las que dependen de cómo esté montado el HTML hoy. Que el título sea nieto de la tarjeta, que el icono vaya dentro del botón, que el pie esté después del cuerpo. Todo eso es estructura, y la estructura cambia constantemente: la cambia un rediseño, la cambia una migración de componentes, la cambia alguien envolviendo algo para arreglar un problema de layout.

Anida las intrínsecas sin límite de profundidad. No anides las extrínsecas, aunque solo sean dos niveles.

flowchart TB
A[Voy a anidar esta regla] --> B{De que depende}
B -- Estado del propio elemento --> S[Anidar sin dudarlo]
B -- Condicion de medio o contenedor --> S
B -- Pseudo elemento propio --> S
B -- Contexto de tema desde arriba --> C[Anidar con el padre a la derecha]
B -- Estructura del HTML debajo --> E[No anidar. Clase propia y regla propia]
style S fill:#a6e3a1,color:#11111b
style C fill:#89b4fa,color:#11111b
style E fill:#f38ba8,color:#11111b

Aplicado al mismo componente, la diferencia se ve enseguida:

/* Mal: cuatro afirmaciones sobre la estructura del HTML. */
.card {
  & .cuerpo {
    & .cabecera {
      & .titulo { font-weight: 600; }
    }
  }
}

/* Bien: el titulo tiene nombre propio y no le importa donde este. */
.card { padding: 1rem; }
.card__titulo { font-weight: 600; }

Y al revés, un bloque profundo que es perfectamente sano porque todo lo que contiene es intrínseco:

.boton {
  --fondo: canvas;
  background: var(--fondo);
  border: 1px solid;
  border-radius: 0.5rem;
  padding: 0.5rem 1rem;

  &:hover { --fondo: color-mix(in oklch, canvas 92%, currentColor); }
  &:focus-visible { outline: 2px solid; outline-offset: 2px; }
  &:disabled { opacity: 0.5; }

  &::after { content: ""; inline-size: 0.5rem; }

  @media (prefers-reduced-motion: no-preference) {
    transition: background 120ms;
    &:active { scale: 0.98; }
  }

  @container (max-inline-size: 20rem) {
    padding: 0.375rem 0.75rem;
  }
}

Hay anidamiento dentro de anidamiento y nadie debería inmutarse: todas las reglas hablan del propio botón. Borrar el bloque borra el botón entero, y ninguna edición de plantilla puede romperlas.

⚠️
El selector de contexto es la excepción bien portada

.tema-oscuro & anida hacia arriba en lugar de hacia abajo, y aunque parece igual de extrínseco, no lo es: el componente necesita saber cómo se comporta bajo un tema, y esa relación es parte de su definición. La diferencia con anidar estructura hacia abajo es que aquí el componente declara cómo reacciona a un contexto conocido, en lugar de afirmar cómo están montadas sus tripas. Mantén esas reglas dentro del bloque y envuelve el selector externo en :where() para que no arrastre peso.

Un orden interno que aguanta

Dentro de un bloque, un orden fijo hace que los bloques largos se lean sin esfuerzo y que las revisiones de código detecten lo que está fuera de sitio. El que uso, y que puedes copiar tal cual:

  1. Custom properties del componente.
  2. Declaraciones, empezando por display y el layout.
  3. Pseudo-clases de estado, en orden de progresión: :hover, :focus-visible, :active, :disabled.
  4. Pseudo-elementos propios.
  5. Variantes del propio elemento con &.modificador.
  6. Selectores de contexto con el padre a la derecha.
  7. Reglas condicionales: @supports, @media, @container.

Y tres convenciones que conviene fijar por escrito porque no hay una respuesta obviamente mejor y lo caro es que cada uno elija la suya:

El & explícito siempre, aunque sea opcional. Cuesta un carácter y elimina de raíz la duda de si .titulo dentro de un bloque es un descendiente o un error de copiar y pegar. También hace que buscar & en un archivo te dé todas las reglas anidadas.

Un nivel de anidamiento estructural como máximo, y solo cuando el hijo no puede tener clase propia porque no lo controlas —contenido de un gestor, HTML generado, un slot.

Nada de anidar por debajo de una condición de medio si la regla no es de la condición. Si dentro de un @media te encuentras anidando estructura, tienes dos problemas superpuestos y conviene separarlos.

Anidar estructura convierte un renombrado en una pérdida silenciosa

El coste que de verdad importa no es la especificidad ni el rendimiento: es que el anidamiento estructural codifica la forma del DOM dentro de la hoja de estilos, y lo hace de una manera que ningún sistema de tipos, ningún compilador y ninguna prueba automática puede verificar. Piensa en qué pasa cuando alguien mete un div extra para arreglar un desbordamiento. En un lenguaje normal, romper una relación entre dos partes produce un error: no compila, la prueba falla, algo se queja. Aquí el sistema sigue funcionando perfectamente, la página carga sin un solo mensaje en la consola, y lo único que ocurre es que una regla ha dejado de encajar. El fallo no se manifiesta como un fallo sino como ausencia de estilo, que es la clase de defecto más cara de detectar porque hay que mirarlo para verlo. Y la asimetría que hay que interiorizar es esta: las condiciones y los estados que anidas —:hover, @container, ::before— tienen una propiedad que la estructura no tiene, y es que no pueden desincronizarse, porque no dependen de ninguna decisión de plantilla que alguien pueda cambiar sin darse cuenta. El estado de hover de un botón seguirá siendo del botón dentro de tres años. Que el título sea nieto y no hijo dura hasta el próximo rediseño. Por eso la profundidad no es la métrica correcta y la naturaleza de la dependencia sí: puedes anidar diez niveles de condiciones y estados sin contraer una sola deuda, y contraerla entera con dos niveles de descendientes. Cuando revises código, no cuentes llaves; pregunta de qué depende cada regla y si eso que depende puede cambiar sin que nadie lo note.

⚔️ Audita un bloque
  1. Coge un componente anidado y clasifica cada regla interna en intrínseca o extrínseca.
  2. Saca las extrínsecas a reglas propias con nombre completo y comprueba que nada cambia visualmente.
  3. Mete un envoltorio extra en el HTML y verifica cuántas reglas dejan de aplicar en cada versión.
  4. Reordena un bloque largo según el orden interno propuesto y mídelo en tiempo de lectura.
  5. Escribe la sección de anidamiento de la guía de estilo de tu equipo en menos de diez líneas.