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

La lista completa de lo que crea un contexto de apilamiento

Las catorce situaciones que forman un stacking context, agrupadas por el motivo por el que existen, y cómo comprobar cuál de ellas se ha disparado en tu página.

⏱ 20 min

Casi todo el mundo conoce tres causas: position con z-index, opacity menor que uno y transform. Son catorce. Y la razón por la que este tema desconcierta no es que la lista sea larga, sino que la mayoría de las causas se disparan por propiedades que nadie asocia con el apilamiento: un filter: blur(0) para forzar la GPU, un will-change metido por rendimiento, un container-type para una container query. Esta lección es la lista entera, ordenada por el motivo por el que cada entrada está ahí.

🎯 Al terminar esta lección sabrás
  • Enumerar las catorce situaciones que crean un contexto de apilamiento.
  • Agrupar las causas según el mecanismo del motor que las provoca.
  • Detectar en una hoja de estilos existente cuáles se han disparado sin querer.
  • Distinguir las que puedes evitar de las que son inherentes al efecto que quieres.

La lista

Un contexto de apilamiento se forma, en cualquier punto del documento, cuando se cumple alguna de estas condiciones.

# Condición
1 El elemento raíz del documento, html.
2 position: absolute o relative con z-index distinto de auto.
3 position: fixed o position: sticky, con o sin z-index.
4 content-visibility: auto, porque aplica containment de layout, style y paint.
5 Ser hijo de un contenedor flex y tener z-index distinto de auto.
6 Ser hijo de un contenedor grid y tener z-index distinto de auto.
7 opacity menor que 1.
8 mix-blend-mode distinto de normal.
9 transform, scale, rotate o translate con valor distinto de none.
10 filter, backdrop-filter o perspective con valor distinto de none.
11 clip-path, mask, mask-image o mask-border con valor distinto de none.
12 isolation: isolate.
13 will-change nombrando cualquier propiedad que crearía un contexto con valor no inicial.
14 contain: layout o contain: paint, o un valor compuesto que los incluya, es decir contain: content y contain: strict.

A las catorce hay que sumar dos casos que no son propiedades declaradas sino estados:

  • Estar en la capa superior. Un dialog abierto con showModal(), un elemento con popover mostrado, un elemento en pantalla completa: todos forman contexto, y su ::backdrop también.
  • Haber animado con @keyframes una propiedad creadora de contexto, con animation-fill-mode: forwards. El contexto persiste después de acabar la animación porque el valor final se mantiene aplicado.

Las filas 9, 10 y 11 son en realidad una sola con muchas propiedades, y las he separado para que la tabla se lea. Lo que las une es que todas exigen componer el subárbol como una imagen antes de colocarlo.

Por qué está cada una en la lista

Agrupar las causas por su mecanismo convierte una lista que hay que memorizar en cuatro ideas que se deducen.

Grupo A: el elemento se pinta en una superficie aparte. Filas 7 a 11, más contain: paint de la 14. Aplicar una opacidad de grupo, un modo de fusión, un filtro, una máscara o una transformación exige que el motor renderice todo el subárbol en un búfer y después lo componga con el resto. Ese búfer es una unidad indivisible: no puede haber otro elemento de la página metido en medio de sus capas internas. Un contexto de apilamiento es exactamente eso, una unidad indivisible, así que la creación del contexto no es un efecto secundario caprichoso: es la definición operativa de lo que acaba de pasar.

Grupo B: el elemento cambia el sistema de coordenadas o el orden. Filas 2, 3, 5, 6. Cuando declaras un z-index estás pidiendo participar en una ordenación explícita, y participar exige que alguien defina el eje. Las filas 5 y 6 existen porque flex y grid son de 2009 y 2011: la especificación original de apilamiento solo contemplaba position, y hubo que añadir explícitamente que un hijo de flex o grid con z-index cuenta como posicionado a efectos de apilamiento aunque su position sea static. Es una de las pocas veces que z-index funciona sin position.

Grupo C: el elemento promete aislamiento. Filas 4, 12 y 14. isolation: isolate es la única cuya razón de ser es crear el contexto y nada más. contain: layout y content-visibility: auto lo crean como consecuencia de una promesa más amplia sobre el subárbol.

Grupo D: el elemento sale del flujo del documento. El caso de la capa superior. Cuando un elemento se promociona al top layer deja de estar en el árbol de pintado normal, y su contexto es hijo directo del contexto raíz.

flowchart TB
A[Por que se creo un contexto aqui] --> B{Que grupo}
B --> C[A El subarbol se pinta en un buffer aparte]
B --> D[B El elemento pide participar en una ordenacion]
B --> E[C El elemento promete aislamiento]
B --> F[D El elemento sale del flujo normal]
C --> G[opacity filter mask blend transform clip-path]
D --> H[position con z-index e hijos de flex y grid]
E --> I[isolation contain content-visibility]
F --> J[capa superior y backdrop]
style A fill:#89b4fa,color:#11111b
style C fill:#f9e2af,color:#11111b
style D fill:#cba6f7,color:#11111b
style E fill:#94e2d5,color:#11111b
style F fill:#a6e3a1,color:#11111b

Las que se disparan sin que lo pidas

Cuatro entradas de la lista causan la mayoría de los bugs porque nadie las escribe pensando en el apilamiento.

will-change. Se añade por rendimiento, con la idea de que es una pista inocua. No lo es: will-change: transform crea un contexto de apilamiento aunque nunca llegues a transformar nada. La propiedad hace que el elemento se comporte como si ya tuviera el valor no inicial. Un will-change: opacity en una tarjeta encierra a su menú desplegable exactamente igual que un opacity: 0.99.

filter. El truco de filter: blur(0) o filter: translateZ(0) para forzar la aceleración por hardware sobrevive en muchas bases de código y crea contexto en cada elemento donde aparece.

content-visibility: auto. Se añade para saltarse el renderizado de lo que está fuera de pantalla. Aplica containment de layout, style y paint, y con el primero llega el contexto de apilamiento. Todo el subárbol de cada bloque diferido queda encerrado.

position: fixed y position: sticky. No hace falta z-index. Un sticky sin más ya es un contexto, y por eso una cabecera pegajosa encierra a sus descendientes: el menú que cuelga de ella no puede pasar por encima de un elemento externo con z-index mayor que el de la cabecera.

Para comprobar cuál se ha disparado en un elemento concreto, DevTools de Chrome tiene una insignia junto al nodo en el panel de elementos y una sección en Computed que indica si el elemento crea contexto y por qué. En Firefox, el inspector marca los nodos que lo crean con una etiqueta. Cuando no tengas herramientas a mano, el método manual es rápido: sube por los ancestros comprobando estas cuatro propiedades y las de la tabla, y el primero que dé positivo es tu techo.

La entrada que se cayó de la lista: container-type

Casi toda la documentación que encuentres —incluida, a día de hoy, la propia lista de MDN— sigue diciendo que container-type: size o inline-size crea un contexto de apilamiento. Ya no es cierto en los navegadores actuales, y merece la pena saberlo porque es un cambio de comportamiento real que rompió layouts en producción.

Cuando se especificaron las container queries, container-type aplicaba containment de layout, y de ahí venían dos efectos implícitos: crear contexto de apilamiento y convertirse en bloque contenedor de los descendientes posicionados. El grupo de trabajo lo consideró un error de diseño y resolvió debilitar el requisito: hoy la especificación dice que container-type aplica containment de estilo y de tamaño y establece un contexto de formato independiente, pero no containment de layout.

El cambio llegó a los motores antes que al texto de la especificación: Chrome 129 en septiembre de 2024, Firefox 133 y Safari 18.4. Desde entonces, un absoluto dentro de un contenedor de consultas ya no se ancla a él, y un z-index alto dentro de él ya no queda encerrado.

Si necesitas el comportamiento antiguo —y a menudo lo necesitas, porque tu layout se apoyaba en él sin saberlo— la forma explícita de recuperarlo es declararlo:

.contenedor {
  container-type: inline-size;
  contain: layout;              /* recupera contexto y bloque contenedor */
}

Y si solo quieres el bloque contenedor sin el contexto de apilamiento, position: relative en el contenedor hace exactamente eso.

La lista crece con cada especificación nueva, y por eso hay que aprender el criterio y no la tabla

Esta lista tenía cinco entradas en CSS 2.1. Hoy tiene catorce y va a seguir creciendo, porque cada vez que la plataforma añade un efecto que requiere componer un subárbol como unidad, la creación de contexto viene incluida en el paquete. Ha pasado con filter, con mix-blend-mode, con contain, con la capa superior. Y de vez en cuando encoge, como acabas de ver: container-type entró en la lista y salió de ella en apenas dos años. Memorizar la tabla te sirve hoy y te va a fallar en tres años. Lo que no caduca es el criterio: si una propiedad obliga al motor a tratar el subárbol como una imagen o como una unidad indivisible, crea contexto de apilamiento, y probablemente también bloque contenedor para los descendientes posicionados y aplanamiento del 3D. Las tres consecuencias viajan juntas porque tienen la misma causa. Cuando te encuentres una propiedad nueva y te preguntes si crea contexto, no busques la tabla: pregúntate si el motor puede seguir pintando los descendientes intercalados con el resto de la página. Si la respuesta es que no, ya lo sabes.

⚔️ Audita tu hoja de estilos
  1. Busca en tu proyecto todas las apariciones de will-change y comprueba cuántos contextos de apilamiento estás creando sin saberlo.
  2. Haz lo mismo con filter, transform y opacity con valores menores que uno.
  3. Añade container-type: inline-size a una tarjeta con un menú absoluto dentro y comprueba que no lo encierra. Añade contain: layout y comprueba que ahora sí.
  4. Aplica will-change: transform a un elemento sin transformarlo nunca y verifica en DevTools que crea contexto.
  5. Comprueba que un elemento con position: sticky y sin z-index ya encierra a sus descendientes posicionados.