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

El árbol de contextos y la comparación por niveles

Cómo se organizan los contextos de apilamiento en una jerarquía paralela al DOM y por qué comparar dos elementos exige subir hasta su contexto común.

⏱ 17 min

Los contextos de apilamiento forman un árbol. No es el árbol del DOM: es un subconjunto suyo, porque solo algunos elementos crean contexto y el resto queda absorbido por el contexto de su antepasado más cercano que sí lo cree. Todo el comportamiento desconcertante de z-index se deduce de una sola regla sobre ese árbol, y una vez la tienes, los bugs dejan de ser sorpresas y pasan a ser consultas.

🎯 Al terminar esta lección sabrás
  • Construir el árbol de contextos de un fragmento de HTML dado.
  • Aplicar la regla de comparación por niveles a dos elementos cualesquiera.
  • Explicar por qué un z-index alto puede perder contra uno bajo.
  • Localizar el contexto común de dos elementos que se solapan mal.

Del árbol del DOM al árbol de contextos

Cada elemento pertenece a exactamente un contexto de apilamiento: el que crea su antepasado más cercano que cumpla alguna de las condiciones de la lista. Si ningún antepasado la cumple, pertenece al contexto raíz, el que crea html.

Los elementos que no crean contexto no aparecen en este árbol. Se dice que quedan asimilados por el contexto de su padre. Un div sin ninguna propiedad especial no es un nodo del árbol de contextos; su z-index, si lo tuviera y no estuviera posicionado, sería ignorado por completo.

Considera este HTML.

<body>
  <header class="barra">
    <nav class="menu">Menú</nav>
  </header>
  <main class="contenido">
    <article class="tarjeta">
      <div class="etiqueta">Nuevo</div>
    </article>
  </main>
  <div class="modal">Modal</div>
</body>
.barra    { position: sticky; inset-block-start: 0; z-index: 10; }
.menu     { position: absolute; z-index: 9999; }
.contenido{ opacity: .98; }
.tarjeta  { position: relative; z-index: 1; }
.etiqueta { position: absolute; z-index: 500; }
.modal    { position: fixed; z-index: 20; }

El árbol de contextos resultante:

flowchart TB
R[Contexto raiz html] --> A[barra sticky z-index 10]
R --> B[contenido opacity 0.98 z-index auto]
R --> C[modal fixed z-index 20]
A --> A1[menu absolute z-index 9999]
B --> B1[tarjeta relative z-index 1]
B1 --> B2[etiqueta absolute z-index 500]
style R fill:#89b4fa,color:#11111b
style A fill:#cba6f7,color:#11111b
style B fill:#cba6f7,color:#11111b
style C fill:#a6e3a1,color:#11111b
style A1 fill:#f9e2af,color:#11111b
style B1 fill:#f9e2af,color:#11111b
style B2 fill:#f38ba8,color:#11111b

Fíjate en .contenido: crea contexto por su opacity de 0,98, pero su z-index es auto porque no está posicionado. En el contexto raíz compite en el paso 6, el de los auto y los ceros. Y .header y .modal tienen z-index de 10 y 20, que están en el paso 7. Ambos quedan por encima de .contenido y de todo su subárbol, sin excepción.

La regla de comparación

Para saber cuál de dos elementos se pinta encima:

  1. Encuentra el contexto de apilamiento común más cercano de los dos.
  2. Sube desde cada elemento hasta llegar a un hijo directo de ese contexto común. Esos dos ancestros son los que compiten.
  3. Compara los z-index de esos dos ancestros, no los de los elementos originales.
  4. Si empatan, gana el que esté después en el orden de documento.

Aplícala a .etiqueta con z-index: 500 y .barra con z-index: 10. El contexto común es el raíz. Subiendo desde .etiqueta llegamos a .contenido, que es hijo directo del raíz con z-index: auto. Subiendo desde .barra llegamos a .barra misma, con z-index: 10. Comparamos auto contra 10: gana .barra. La etiqueta con 500 queda debajo de una barra con 10, y no hay número que puedas escribir en .etiqueta que lo cambie.

La analogía que mejor funciona es la de los números de versión. Los z-index de la cadena de ancestros forman una tupla, y la comparación es lexicográfica:

  • .etiqueta tiene la tupla auto, 1, 500 — o si prefieres, 0.1.500.
  • .barra tiene la tupla 10, es decir 10.0.0.
  • .menu tiene la tupla 10, 9999, es decir 10.9999.
  • .modal tiene 20.

Ordenadas: 0.1.500 menor que 10.0.0 menor que 10.9999 menor que 20. El modal está encima de todo, el menú encima de la barra, y la etiqueta debajo de todo aunque tenga el número más aparatoso. La comparación se decide en el primer componente que difiere, exactamente igual que con las versiones semánticas.

Cómo se lee esto en la práctica

El procedimiento manual, cuando algo se solapa mal, tiene tres pasos y no falla.

Primero, identifica los dos elementos que compiten de verdad. No son los que ves solaparse: son sus ancestros a nivel del contexto común. En el ejemplo, no compiten la etiqueta y la barra, compiten .contenido y .barra.

Segundo, comprueba si el techo es evitable. Si .contenido crea contexto por un opacity: .98 que estaba ahí por un efecto de aparición que ya no se usa, quitarlo resuelve el problema de raíz y sin números mágicos.

Tercero, si el techo no es evitable, sube el número del ancestro, no el del elemento. Es la conclusión operativa de toda la lección: el z-index que hay que tocar casi nunca es el del elemento que no se ve.

Chrome DevTools tiene una vista dedicada para esto en el panel Layers, que muestra la composición real, y una insignia junto a los nodos del panel Elements que marca los que crean contexto. Firefox marca los nodos creadores en el inspector. Cuando el árbol es grande, esas herramientas ahorran el recorrido manual, pero conviene saber hacerlo a mano porque a menudo el problema está en un ancestro que no habrías mirado.

Un contexto de apilamiento es una barrera, y las barreras son diseño, no accidente

La lectura pesimista de todo esto es que los contextos de apilamiento son una trampa. La lectura útil es la contraria: son el único mecanismo que impide que el z-index de una página sea un espacio de nombres global. Piensa qué pasaría sin ellos: cualquier componente de cualquier librería que escribiera z-index: 100 competiría directamente con tu modal, tu barra y tu tooltip, y la única forma de coexistir sería que todo el ecosistema se pusiera de acuerdo en un registro central de números. Es exactamente el problema que tuvo CSS antes de @layer con la especificidad, y antes de los módulos con los nombres de clase. El contexto de apilamiento es a z-index lo que el ámbito léxico es a las variables: un mecanismo de encapsulación que convierte un número global en un número local. Cuando lo asumes, dejas de escribir z-index: 9999 y empiezas a hacer lo que el mecanismo pide: definir contextos deliberadamente en las fronteras de tus componentes, con isolation: isolate, y usar dentro de cada uno una escala pequeña de tres o cuatro valores que solo tiene sentido ahí.

⚔️ Recorre el árbol
  1. Reproduce el HTML y el CSS del ejemplo y verifica que la etiqueta con 500 queda debajo de la barra con 10.
  2. Quita el opacity de .contenido y comprueba que la etiqueta pasa por encima sin tocar su z-index.
  3. Escribe la tupla de z-index de cada uno de los cinco elementos y ordénalas a mano; compara con lo que ves.
  4. Activa la insignia de contexto de apilamiento en DevTools y localiza los tres nodos creadores.
  5. Añade un filter: saturate(1.1) a .tarjeta y explica sin ejecutar qué cambia en el árbol.