wandres.dev
CONTEXTOS DE APILAMIENTO · z-index y por qué no funciona

isolation y la arquitectura de capas de una página

Crear contextos de apilamiento a propósito con isolation: isolate, y diseñar una escala de z-index que quepa en la cabeza y no crezca sola.

⏱ 17 min

Todas las lecciones anteriores tratan los contextos de apilamiento como algo que te pasa. Esta los trata como algo que decides. isolation: isolate es la única propiedad de la lista cuyo único efecto es crear un contexto: no cambia el pintado, ni el layout, ni la composición, ni tiene coste de rendimiento apreciable. Es un delimitador de ámbito puro, y con ella se construye la arquitectura de capas que hace que una página grande no acumule números mágicos.

🎯 Al terminar esta lección sabrás
  • Usar isolation: isolate para acotar el apilamiento de un componente.
  • Explicar su segundo uso, aislar grupos de mix-blend-mode.
  • Diseñar una escala de z-index con nombres en lugar de números.
  • Decidir qué elementos merecen un contexto propio y cuáles no.

Un contexto sin efectos secundarios

Antes de isolation, crear un contexto de apilamiento a propósito requería un efecto secundario: position: relative; z-index: 0, que además convierte al elemento en bloque contenedor de sus descendientes absolutos; o opacity: .999, que fuerza al motor a componer el subárbol en un búfer y cuesta memoria; o transform: translateZ(0), que además puede promocionar a capa de composición y cambia el redondeo de subpíxel del texto.

isolation: isolate no hace nada de eso. Solo crea el contexto.

.componente { isolation: isolate; }

Con esa línea, cualquier z-index que escribas dentro de .componente se resuelve entre hermanos de .componente y no puede escapar hacia arriba ni ser invadido desde fuera. El componente pasa a tener su propio espacio de nombres de profundidad.

El caso de uso canónico: una tarjeta con una imagen de fondo, un degradado por encima y texto sobre ambos.

.tarjeta { isolation: isolate; position: relative; }
.tarjeta img      { position: absolute; inset: 0; z-index: 0; }
.tarjeta .velo    { position: absolute; inset: 0; z-index: 1; }
.tarjeta .titulo  { position: relative; z-index: 2; }

Los números 0, 1 y 2 son legibles, describen la intención y no significan nada fuera de la tarjeta. Puedes meter la tarjeta en cualquier página sin preguntarte con qué compite. Sin el isolation, esos mismos números competirían con todo lo que haya en el contexto raíz, y bastaría que un componente vecino usara un z-index: 1 para que las capas se entrelazaran.

El segundo uso: aislar la fusión

isolation no nació para el apilamiento. Nació para mix-blend-mode, y ese sigue siendo su significado más literal.

Un elemento con mix-blend-mode se funde con el grupo de apilamiento en el que está, es decir, con todo lo que haya pintado por debajo dentro de su contexto de apilamiento. Si ese contexto es el raíz, se funde con el fondo de la página entera, que rara vez es lo que quieres.

/* el texto se funde con el fondo de la página, no solo con la imagen */
.titulo { mix-blend-mode: difference; }

Envolviendo la imagen y el texto en un contenedor con isolation: isolate, el grupo de fusión pasa a ser ese contenedor: el texto se funde con la imagen y con nada más.

.cabecera { isolation: isolate; position: relative; }
.cabecera img { position: absolute; inset: 0; }
.cabecera .titulo { position: relative; mix-blend-mode: difference; }

Los dos usos son la misma operación vista desde dos lados. Un contexto de apilamiento es también un grupo de fusión, porque las dos cosas necesitan lo mismo: una frontera donde el subárbol se comporte como una unidad.

Una escala que no crece sola

El problema que resuelve la arquitectura de capas no es técnico, es de mantenimiento: los números de apilamiento crecen porque cada vez que alguien tiene un conflicto elige un número mayor que el que ve. Con custom properties y una lista corta, ese crecimiento se corta de raíz.

:root {
  --z-contenido:   0;
  --z-pegajoso:    100;
  --z-desplegable: 200;
  --z-superpuesto: 300;
}

Cuatro nombres para toda la página. La regla es que solo estos cuatro valores se usan en el contexto raíz; cualquier otro z-index del proyecto tiene que vivir dentro de un componente aislado y ser un número pequeño y local.

.barra   { position: sticky; z-index: var(--z-pegajoso); }
.selector{ isolation: isolate; }                 /* ámbito local */
.selector .lista { position: absolute; z-index: 2; }  /* local, sin conflicto global */
flowchart TB
A[Necesitas un z-index nuevo] --> B{Debe competir con toda la pagina}
B -->|No, solo dentro de su componente| C[isolation isolate en la raiz del componente]
C --> D[Numeros locales pequenos del 1 al 3]
B -->|Si, debe flotar sobre todo| E{Es un elemento de superposicion}
E -->|Si, modal menu tooltip toast| F[Capa superior con dialog o popover]
F --> G[Sin z-index]
E -->|No, es estructura de pagina| H[Usa una de las cuatro variables de la escala]
style A fill:#89b4fa,color:#11111b
style C fill:#a6e3a1,color:#11111b
style D fill:#a6e3a1,color:#11111b
style G fill:#a6e3a1,color:#11111b
style H fill:#f9e2af,color:#11111b

La regla operativa que hace que esto se sostenga con el tiempo: si un componente necesita un z-index mayor que 3, casi siempre es que le falta un isolation o que ese elemento debería estar en la capa superior. Tratar el número grande como un error de diseño y no como una solución es lo que impide que la escala se dispare.

Un detalle sobre dónde poner el isolation: va en la raíz del componente, no en un envoltorio arbitrario, y hay que asegurarse de que todos los elementos que participan en el apilamiento local están dentro. Si el desplegable de un selector se renderiza en un portal fuera del componente, el isolation no lo alcanza y hay que tratarlo como superposición global, es decir, llevarlo a la capa superior.

isolate no es gratis en un sitio, y conviene saber cuál

isolation: isolate no tiene coste de composición: el motor no crea ningún búfer, no promociona ninguna capa, no cambia nada del pintado. En eso es genuinamente el aislante más barato de la lista. Pero no es neutro para los descendientes con position: fixed… y aquí está el matiz que conviene tener claro, porque circula la afirmación contraria. Crear un contexto de apilamiento y crear un bloque contenedor para descendientes fijos son dos cosas distintas: lo segundo lo provocan transform, filter, perspective, backdrop-filter, will-change con esas propiedades y contain: layout o paint; pero no isolation, ni opacity, ni mix-blend-mode. Un position: fixed dentro de un isolation: isolate sigue anclado al viewport. Lo que sí cambia es su apilamiento: ese fijo queda encerrado en el contexto del componente y ya no puede pasar por encima de lo que haya fuera. Es decir, isolation te encierra la profundidad pero te respeta la geometría, mientras que transform te encierra las dos. Distinguir esas dos consecuencias es lo que te permite elegir el aislante correcto: si solo quieres acotar z-index, isolation; si además quieres que los fijos de dentro se anclen al componente, necesitas una de las otras, y estás pagando un coste de composición real a cambio.

⚔️ Aísla un componente de verdad
  1. Toma un componente de tu proyecto con z-index mayores que 10 y añade isolation: isolate en su raíz. Baja los números a 1, 2 y 3 y comprueba que nada se rompe.
  2. Aplica mix-blend-mode: difference a un título sin aislar y observa que se funde con el fondo de la página. Aísla el contenedor y compara.
  3. Define la escala de cuatro variables y sustituye todos los z-index sueltos del contexto raíz.
  4. Mete un position: fixed dentro de un isolation: isolate y verifica que sigue anclado al viewport.
  5. Repite el punto anterior con transform: translateZ(0) en lugar de isolation y comprueba la diferencia.