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

El orden de pintado: las siete capas dentro de un contexto

El algoritmo que decide qué se pinta encima de qué cuando nadie ha escrito un z-index, y por qué un elemento posicionado tapa a uno flotante sin pedirlo.

⏱ 18 min

Antes de discutir z-index hay que entender que el navegador ya tiene un orden de pintado completo sin él. Está definido desde CSS 2.1 en el apéndice E, tiene siete pasos, y explica solapamientos que la mayoría atribuye a la magia: por qué un position: relative sin z-index tapa a sus hermanos, por qué el texto de un párrafo se pinta encima del fondo de un flotante pero el flotante encima del fondo del párrafo, y por qué el orden de documento decide los empates.

🎯 Al terminar esta lección sabrás
  • Recitar los siete pasos del orden de pintado dentro de un contexto de apilamiento.
  • Explicar la diferencia entre z-index: auto y z-index: 0 en términos del algoritmo.
  • Predecir el solapamiento de dos elementos sin ningún z-index declarado.
  • Identificar en qué paso se pinta un elemento dado.

Los siete pasos

Dentro de un contexto de apilamiento, el motor pinta en este orden. Lo que va después se dibuja encima de lo que va antes.

  1. El fondo y los bordes del elemento que forma el contexto. Es el lienzo sobre el que se pinta todo lo demás del subárbol.
  2. Los contextos hijos con z-index negativo, ordenados de más negativo a menos. Esta es la razón por la que un z-index: -1 se mete detrás del fondo del padre solo si el padre no forma contexto; si lo forma, el hijo negativo queda por encima del fondo del padre pero por debajo de todo lo demás.
  3. Los descendientes de bloque en flujo normal y sin posicionar. Sus fondos y bordes.
  4. Los flotantes no posicionados, con sus contenidos.
  5. Los descendientes de nivel en línea en flujo normal: texto, span, imágenes en línea.
  6. Los descendientes posicionados con z-index: auto o z-index: 0, y los contextos hijos con z-index: 0. Todos en el mismo cajón, resueltos entre sí por orden de documento.
  7. Los contextos hijos con z-index positivo, de menor a mayor.

Dos consecuencias saltan a la vista en cuanto lees la lista con atención.

La primera: el paso 4 va antes que el paso 5. Un flotante se pinta por debajo del texto en línea que lo rodea. Por eso un flotante con fondo no tapa las palabras que fluyen a su alrededor; las palabras están en una capa posterior. Pero sí tapa el fondo del bloque que lo contiene, que se pintó en el paso 3.

La segunda: el paso 6 va después del 3, 4 y 5. Cualquier elemento posicionado, aunque no le hayas puesto z-index, se pinta por encima de todo el contenido no posicionado. Esto explica el truco más viejo del CSS: si dos cajas se solapan y quieres que una gane, basta con darle position: relative. No hace falta ningún z-index.

/* .a se pinta encima de .b sin tocar z-index: está en el paso 6 y .b en el 3 */
.a { position: relative; }
.b { background: crimson; }

auto no es cero

z-index: auto y z-index: 0 colocan al elemento en el mismo paso, el 6, y por eso muchísimas veces se comportan igual. La diferencia es otra y es fundamental: z-index: 0 en un elemento posicionado crea un contexto de apilamiento y auto no.

Un elemento con position: relative; z-index: auto es transparente al apilamiento: sus descendientes posicionados compiten directamente con los hermanos del elemento, en el contexto de apilamiento del abuelo. Si le cambias el z-index de auto a 0 —un cambio que parece inocuo, porque el número es el que ya estaba implícito— acabas de encerrar todo su subárbol: sus descendientes ya no pueden salir por encima de nada externo.

.tarjeta      { position: relative; }              /* transparente */
.tarjeta .menu{ position: absolute; z-index: 100; }/* compite globalmente */

.tarjeta-b    { position: relative; z-index: 0; }  /* forma contexto */
.tarjeta-b .menu { position: absolute; z-index: 100; } /* atrapado dentro */

Este par de reglas es el origen de una fracción notable de los bugs de menús desplegables que se cortan. Alguien añade z-index: 0 en un reset o en una utilidad de layout, y a partir de ahí ningún descendiente puede sobresalir.

flowchart TB
A[Fondo y bordes del elemento raiz del contexto] --> B[Contextos hijos con z-index negativo]
B --> C[Bloques en flujo sin posicionar]
C --> D[Flotantes no posicionados]
D --> E[Contenido en linea sin posicionar]
E --> F[Posicionados con z-index auto o cero]
F --> G[Contextos hijos con z-index positivo]
style A fill:#89b4fa,color:#11111b
style B fill:#cba6f7,color:#11111b
style C fill:#94e2d5,color:#11111b
style D fill:#94e2d5,color:#11111b
style E fill:#94e2d5,color:#11111b
style F fill:#f9e2af,color:#11111b
style G fill:#a6e3a1,color:#11111b

Los empates y el orden de documento

Dentro de un mismo paso, el criterio de desempate es el orden en el árbol del documento: lo que aparece después se pinta encima. No hay ningún otro criterio; ni la especificidad, ni el orden de las reglas CSS, ni el momento en que se insertó el nodo si después se movió.

Esto tiene una implicación arquitectónica que conviene explotar: si el orden del DOM ya expresa la jerarquía visual, no necesitas ningún z-index. Una lista de elementos superpuestos que deben apilarse en el orden en que aparecen —una baraja de cartas, una pila de notificaciones, un stack de avatares— se resuelve con position: relative en todos y nada más. Cero números mágicos que mantener.

El caso inverso, cuando quieres invertir el orden visual respecto al del DOM, es donde z-index empieza a ganarse el sueldo. Y aun así hay una alternativa poco conocida: dentro de un contenedor flex o grid, la especificación define el orden de pintado en términos del orden de documento modificado por order, no del orden de documento crudo. Cambiar order en un hijo de flex cambia por tanto su posición en el desempate del paso 6, sin tocar el DOM ni escribir un solo z-index. Es la única propiedad de layout que reordena la pintura.

El orden de pintado es un recorrido, no una comparación global

La intuición equivocada que arrastra casi todo el mundo es imaginar que el navegador toma todos los elementos de la página, los ordena por z-index como si fuera una lista, y los pinta. No es eso. El motor hace un recorrido en profundidad del árbol de contextos de apilamiento, y dentro de cada contexto ejecuta los siete pasos de arriba. Un elemento nunca se compara con otro que esté en un contexto distinto: se comparan los contextos entre sí, en el nivel donde son hermanos, y el resultado de esa comparación arrastra a todos sus descendientes en bloque. La analogía exacta es la de los números de versión: comparar 2.99.99 con 3.0.0 no requiere mirar los dígitos menores, porque el primer componente ya decide. Por eso un z-index: 999999 dentro de un contexto cuyo antepasado tiene z-index: 1 pierde siempre contra un z-index: 2 que sea hermano de ese antepasado. Y por eso la pregunta correcta ante un bug de apilamiento nunca es cuánto z-index tiene este elemento, sino en qué contexto vive y qué antepasado suyo es el que realmente está compitiendo.

⚔️ Predice el solapamiento sin z-index
  1. Coloca dos divs solapados con márgenes negativos, sin ningún position, y comprueba cuál gana. Añade position: relative al primero y verifica el cambio.
  2. Haz que un flotante con fondo se solape con el texto del párrafo que lo rodea y confirma que el texto se pinta encima.
  3. Toma un position: relative con un hijo position: absolute; z-index: 50, y un hermano del padre con z-index: 10. Añade z-index: 0 al padre y observa qué se rompe.
  4. Construye una pila de cinco avatares solapados usando solo position: relative y el orden del DOM.
  5. En un contenedor flex, invierte visualmente el apilamiento de dos hijos usando order y comprueba que el orden de pintado sigue al de order.