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

Por qué tu z-index de 9999 sigue quedando debajo

El método de diagnóstico para bugs de apilamiento, los cinco casos que cubren el noventa por ciento de ellos, y por qué subir el número nunca es la solución.

⏱ 16 min

El síntoma es siempre el mismo: un menú, un tooltip o un modal que se corta o queda por debajo de algo. La reacción también: subir el número. 100, 999, 9999, 2147483647. Y no funciona, porque el número nunca fue el problema. Esta lección es el método para localizar la causa real en menos de un minuto y los cinco casos concretos que cubren casi todos los incidentes que verás.

🎯 Al terminar esta lección sabrás
  • Aplicar un procedimiento de diagnóstico reproducible ante un bug de apilamiento.
  • Reconocer los cinco patrones de fallo más frecuentes por su síntoma.
  • Elegir entre las cuatro soluciones reales según el caso.
  • Justificar por qué subir el z-index no arregla ninguno de los cinco.

El método

Ante un elemento que no se ve o queda debajo de lo que no debería:

Paso 1. Comprueba que el problema es de apilamiento y no de recorte. Son dos bugs distintos con el mismo aspecto. Si el elemento aparece cortado por el borde de un ancestro, no es apilamiento: es un overflow distinto de visible en algún ancestro, que recorta y punto. Ningún z-index arregla un recorte. Si el elemento aparece entero pero por detrás de otro, entonces sí es apilamiento.

Paso 2. Sube por la cadena de ancestros buscando el primer creador de contexto. Ese elemento es tu techo: todo lo que hay dentro de él está encerrado. Con la insignia de DevTools es inmediato; a mano, revisa opacity, transform, filter, will-change, contain, content-visibility, isolation, position: fixed o sticky, y position con z-index.

Paso 3. Compara el z-index de ese techo con el del elemento que te tapa. Ahí está la comparación real. Si el techo tiene auto y el rival tiene 5, el rival gana siempre.

Paso 4. Decide entre las cuatro salidas. Y solo hay cuatro.

flowchart TB
A[El elemento no se ve donde deberia] --> B{Aparece cortado o entero por detras}
B -->|Cortado| C[Es overflow no apilamiento]
C --> D[Busca overflow distinto de visible en los ancestros]
B -->|Entero por detras| E[Sube hasta el primer ancestro que crea contexto]
E --> F{Ese contexto es evitable}
F -->|Si| G[Quita la propiedad que lo crea]
F -->|No| H{Puedes subir el z-index del techo}
H -->|Si| I[Sube el del techo no el del elemento]
H -->|No| J[Saca el elemento del subarbol]
J --> K[Capa superior con dialog o popover]
style A fill:#f38ba8,color:#11111b
style C fill:#f9e2af,color:#11111b
style G fill:#a6e3a1,color:#11111b
style I fill:#a6e3a1,color:#11111b
style K fill:#a6e3a1,color:#11111b

Las cuatro salidas, en orden de preferencia: quitar la propiedad que crea el contexto si sobraba; subir el z-index del techo en lugar del elemento; sacar el elemento del subárbol moviéndolo en el DOM; o promocionarlo a la capa superior con dialog o popover, que es la respuesta correcta para cualquier cosa que deba flotar sobre toda la página.

Los cinco casos

Caso 1: el ancestro transformado. Una tarjeta con transform: translateY(-2px) en :hover para el efecto de elevación. El menú desplegable de dentro deja de salir por encima de la tarjeta siguiente solo al pasar el ratón. El síntoma intermitente es la firma: la propiedad que crea el contexto solo está aplicada en un estado. Solución: mover la transformación a un hijo envoltorio que no contenga el menú, o usar box-shadow para la elevación.

Caso 2: la opacidad de transición. Un contenedor con opacity: 0 que se anima a 1 al aparecer. Mientras la animación corre, y si animation-fill-mode es forwards también después, crea contexto. El menú de dentro queda encerrado. Solución: animar la opacidad en un hijo, no en el contenedor de los elementos flotantes.

Caso 3: la barra pegajosa. position: sticky crea contexto sin z-index. Un desplegable que cuelga de la barra no puede pasar por encima de un elemento externo con z-index mayor que el de la barra. Solución: dar a la barra un z-index suficientemente alto en su propio contexto, o llevar el desplegable a la capa superior.

Caso 4: el will-change de rendimiento. Alguien añadió will-change: transform a una lista larga para que el scroll fuera fluido. Cada fila es ahora un contexto de apilamiento y ningún tooltip de fila puede salir de su fila. Solución: quitar el will-change —casi siempre sobra— o mover los tooltips fuera.

Caso 5: el aislamiento por rendimiento. Un contain: content o un content-visibility: auto añadidos para acelerar una lista larga encierran cada bloque diferido. El síntoma es especialmente confuso porque las dos propiedades no tienen nada que ver con el apilamiento a primera vista. Solución: aplicar el containment a un envoltorio que no contenga al flotante, o llevar el flotante a la capa superior. Ojo con la variante que ya no produce este bug: container-type: inline-size lo provocaba hasta que Chrome 129, Firefox 133 y Safari 18.4 le quitaron el containment de layout, así que si arrastras un diagnóstico antiguo, verifícalo antes.

Los cinco tienen algo en común: la propiedad culpable no es un z-index. Por eso subir el número no hace nada. El número que subes vive dentro de una caja de la que no puede salir.

Lo que sí funciona

Si el elemento debe flotar por encima de toda la página —un modal, un menú, un tooltip, un toast— la respuesta correcta en 2026 no es un z-index grande: es la capa superior. Un elemento con el atributo popover mostrado, o un dialog abierto con showModal(), se pinta por encima de todo el documento independientemente de dónde esté en el DOM y sin que ningún contexto de apilamiento intermedio pueda encerrarlo. El z-index deja de ser relevante para ese elemento.

<button popovertarget="menu">Opciones</button>
<div id="menu" popover>
  <button>Duplicar</button>
  <button>Archivar</button>
</div>

Este menú sale por encima de cualquier cosa aunque su botón esté dentro de una tarjeta transformada, dentro de un contenedor de consultas, dentro de una barra pegajosa. No hay z-index en el ejemplo y no hace falta.

Cuando el elemento no debe flotar sobre toda la página sino solo dentro de su componente, la solución es la contraria: crear un contexto deliberadamente en la frontera del componente con isolation: isolate y usar dentro una escala pequeña de números que solo significa algo ahí. Eso es el tema de la siguiente lección.

El z-index grande es una confesión, no una solución

Hay una señal que vale la pena aprender a leer en revisiones de código. Un z-index: 9999 no significa que el autor necesitara estar por encima de nueve mil novecientas noventa y ocho cosas: significa que no sabía contra qué estaba compitiendo, y eligió un número grande con la esperanza de ganar por goleada. Es la misma pulsión que lleva a escribir !important, y falla por la misma razón: los dos mecanismos son locales, y un valor extremo dentro de un ámbito equivocado sigue perdiendo contra un valor pequeño en el ámbito correcto. La consecuencia práctica es que una base de código con números de apilamiento grandes es una base de código donde nadie sabe cuál es la jerarquía visual, y donde cada bug nuevo se resuelve subiendo el número hasta que la escala se agota. La cura no es una convención de números: es reducir a tres o cuatro los contextos que de verdad importan a nivel de página —contenido, cabecera, superposiciones— y llevar todo lo demás o bien a la capa superior, o bien a un contexto local aislado donde el número vuelva a ser pequeño y legible.

⚔️ Reproduce y arregla los cinco casos
  1. Monta el caso 1: tarjeta con transform en :hover y menú absoluto dentro. Confirma que el bug solo aparece al pasar el ratón.
  2. Arréglalo sin tocar ningún z-index, moviendo la transformación a otro elemento.
  3. Reproduce el caso 4 con will-change: transform en las filas de una tabla y comprueba que ningún tooltip sale.
  4. Sustituye ese tooltip por un elemento con popover y verifica que ahora sale sobre todo.
  5. Escribe un caso con overflow: hidden que se parezca a un bug de apilamiento y demuestra con el paso 1 del método que no lo es.