Por qué tu z-index no funciona
Depura por qué z-index no funciona con las herramientas de desarrollo. Detecta el contexto de apilamiento que limita el elemento y corrige modales, menús y encabezados fijos.
Si un elemento con z-index: 9999 sigue renderizándose detrás de otro con valor 2, es porque un ancestro del primer elemento ha creado un contexto de apilamiento, y un z-index nunca tiene efecto fuera del contexto al que pertenece.
Ya probaste con 99999. Luego con 2147483647. El modal sigue quedando debajo del header, y nada en el panel Styles parece estar mal.
Para la teoría del orden de apilamiento, consulta CSS: Demystifying the z-index property. Esta guía cubre la parte de depuración: cómo visualizar los contextos de apilamiento en DevTools, cómo recorrer el DOM hacia arriba hasta encontrar el ancestro que atrapa a tu elemento y cómo resolver cada uno de los casos habituales sin aumentar el número.
Puntos clave
- Un valor de z-index solo compite con elementos del mismo contexto de apilamiento, por lo que
9999pierde frente a2cuando un ancestro del primer elemento tiene su propio contexto de apilamiento. - Cuando un z-index más alto pierde, compara los ancestros de ambos elementos que comparten un padre común. Los valores propios de cada elemento no lo deciden.
- Cualquier
transform,filteroclip-pathdistinto denone, o unaopacityinferior a 1, crea un nuevo contexto de apilamiento aunque el elemento no tengapositionniz-index. - Un dropdown que se corta en el borde de su tarjeta suele estar recortado por
overflow, y ningún valor de z-index lo solucionará. - Convertir un overlay en hijo directo de
<body>lo saca del contexto de apilamiento de todos sus ancestros.
¿Cuál es la regla que explica por qué z-index no funciona?
Un z-index solo ordena un elemento respecto a los demás miembros de su propio contexto de apilamiento. Un contexto de apilamiento es un grupo que el navegador pinta como una sola unidad. La guía de MDN sobre contextos de apilamiento enumera las propiedades que crean uno. Cuando un ancestro crea un contexto, el z-index del hijo solo se aplica dentro de ese ancestro. Así que si el ancestro queda por debajo de otro contexto, el hijo también queda por debajo, sin importar el número que le asignes.
Esta es la versión mínima del bug:
<header style="position: fixed; top: 0; z-index: 2; background: tomato;">Header</header>
<main style="opacity: 0.99;">
<div class="modal" style="position: fixed; inset: 0; z-index: 9999; background: rgb(0 0 0 / 0.6);">
Modal
</div>
</main>
El modal pierde porque main tiene opacity: 0.99, lo que convierte a main en un contexto de apilamiento. Dentro de main, el 9999 del modal funciona perfectamente. Pero main en sí se compara con el header en el nivel raíz, y el 2 del header es mayor. Cuando un z-index más alto pierde, fíjate en los ancestros: busca el ancestro más cercano de cada elemento que comparta un padre común con el otro y compara esos dos.
¿Cómo se visualizan los contextos de apilamiento en DevTools?
Las DevTools de Chromium ofrecen herramientas visuales para investigar problemas de superposición, lo que puede ser más rápido que ir a ciegas. Tanto Chrome como Edge abren estas vistas desde el Command Menu, con Command+Shift+P en macOS o Control+Shift+P en Windows y Linux. Si quieres conocer más paneles de DevTools que suelen pasar desapercibidos, consulta Hidden Gems in Chrome DevTools.
Chrome: el panel Layers
- Abre DevTools y pulsa Command+Shift+P o Control+Shift+P.
- Escribe
Layersy elige Show Layers panel. También puedes ir a More options > More tools > Layers panel. - Rota el lienzo y selecciona una capa para ver sus detalles, incluido el motivo por el que el navegador le asignó su propia capa.
El panel Layers muestra a la izquierda un árbol con las capas de la página y, a su lado, un diagrama 3D de ellas. Usa el panel para inspeccionar cómo el navegador ha dividido la página en capas de composición, pero recuerda que no son lo mismo que los contextos de apilamiento. Para encontrar el ancestro responsable de un contexto de apilamiento, usa el método de recorrido ascendente que se describe más abajo. El panel también aparece en la depuración de animaciones CSS entrecortadas.
Edge: 3D View
- Abre el Command Menu y ejecuta Show 3D View.
- Selecciona la pestaña Z-index.
- Activa la opción Show all para ver los elementos posicionados junto a los contextos de apilamiento.
La 3D View de Edge tiene tres pestañas: Composited Layers, Z-index y DOM. La pestaña Z-index dibuja en 3D los contextos de apilamiento de la página, de modo que un elemento atrapado aparece anidado dentro de una placa inferior.
Si quieres una vista en árbol de los contextos de apilamiento en Chrome o Firefox, la extensión CSS Stacking Context inspector añade un panel con el árbol de contextos, además de una barra lateral en Elements que explica por qué el elemento seleccionado crea un contexto. Su documentación indica que todavía no puede inspeccionar el interior de iframes de terceros.
El método de recorrido ascendente por los ancestros
El método de recorrido ascendente localiza el ancestro que atrapa al elemento usando únicamente el panel Elements y el panel Computed. Funciona igual en todos los navegadores.
- Selecciona el elemento que pierde (el modal, el menú).
- En Elements, haz clic en su padre.
- Abre el panel Computed y escribe en el cuadro de filtro cada propiedad de la checklist que aparece más abajo:
transform,opacity,filter,position,z-index, etc. - Si ninguna crea un contexto, sube un nivel y repite hasta llegar a
<html>. - Anota el primer ancestro que crea un contexto.
- Repite los pasos 1 a 5 empezando por el elemento que gana.
- Las dos cadenas se encuentran en un ancestro común. Sus dos hijos situados en esas cadenas son los elementos que realmente se comparan.
Si trabajas a partir de un reporte de bug en lugar de una reproducción local, el session replay muestra el síntoma desde el lado del usuario, como un clic que cae sobre un overlay invisible o un menú que se abre detrás del contenido. Eso te indica qué elemento seleccionar en el paso 1.
Checklist: propiedades que crean un contexto de apilamiento
Comprueba cada ancestro con esta tabla. Basta con que coincida una fila para atrapar a sus descendientes.
| Propiedad | Crea un contexto cuando | Qué buscar en Computed |
|---|---|---|
position: absolute / relative | z-index no es auto | Ambos valores definidos en el mismo elemento |
position: fixed / sticky | Siempre | Solo el valor de position |
opacity | Es inferior a 1 | Valores como 0.99, a menudo restos de una animación de fundido |
transform, scale, rotate, translate | Cualquier valor distinto de none | Un valor matrix(...), incluidas las transformaciones identidad |
filter, backdrop-filter | Distinto de none | blur(), drop-shadow() |
clip-path, perspective | Distinto de none | Cualquier forma o longitud |
mask, mask-image, mask-border | Distinto de none | Valores de imagen o degradado |
will-change | Incluye una propiedad que crearía un contexto si la definieras | will-change: transform u opacity |
isolation | isolate | Valor explícito |
contain | layout, paint, strict o content | Habitual en listas virtualizadas |
| Hijo de flex o grid | z-index no es auto | display: flex o grid en el padre. No hace falta position |
La lista completa de MDN también incluye mix-blend-mode distinto de normal, los elementos del top layer y los elementos cuyas propiedades generadoras de contexto se animan con animation-fill-mode: forwards. MDN todavía menciona container-type: size e inline-size, pero el CSS Working Group eliminó la contención de layout de container-type, por lo que ya no crea un contexto de apilamiento. Chrome aplicó ese cambio en la versión 129, y Firefox y Safari lo han adoptado después.
Tres casos habituales: síntoma, causa y solución
Modal detrás de un header
Síntoma: el overlay oscurece la página, pero el header sigue brillante por encima. Causa: el modal se renderiza dentro de un wrapper que tiene un transform, una opacity o un z-index posicionado, y el contexto del header supera a ese wrapper. Solución: mueve el modal a <body> (consulta las soluciones más abajo).
Dropdown recortado dentro de una tarjeta con transform
Un dropdown dentro de una tarjeta transformada tiene uno de dos problemas, apilamiento o recorte, así que primero determina cuál es el tuyo. Si el menú se pinta detrás de la siguiente tarjeta, se trata de apilamiento: la tarjeta transformada es un contexto, y las tarjetas que aparecen después en el DOM se pintan encima. Eleva la tarjeta abierta:
.card.is-open { position: relative; z-index: 1; }
Si el menú se corta en el propio borde de la tarjeta, se trata de recorte. Revisa overflow en la tarjeta y en sus ancestros. Cualquier valor distinto de visible recorta el menú, y ningún valor de z-index lo recuperará.
Elemento sticky que no se mantiene encima
Síntoma: las tarjetas se deslizan por encima de un header sticky al pasar el cursor o al hacer scroll. Causa: cualquier elemento con position: fixed o sticky crea un contexto. Sin embargo, sin z-index se sitúa en el nivel 0, y los hermanos posteriores que crean contextos (como tarjetas con transforms en hover) se pintan encima porque aparecen después en el DOM. Solución:
.site-header { position: sticky; top: 0; z-index: var(--z-sticky); }
Si el header no se queda fijo en absoluto, la causa suele ser el overflow de algún ancestro. Para construir un header sticky completo, consulta Building a responsive sticky navbar with CSS.
Tres formas de liberar un elemento atrapado
Todas las soluciones para un elemento atrapado modifican la estructura de contextos de apilamiento en lugar del número de z-index: mover el elemento, mover la propiedad o colocar deliberadamente un límite donde lo necesites.
Mueve el elemento en el DOM. Un overlay que es hijo directo de <body> no tiene ningún contexto ancestro por encima, así que su z-index se compara en la raíz. Por eso los portales funcionan tan bien para overlays que deben cubrir toda la página:
const modal = document.querySelector('.modal');
function openModal() {
document.body.appendChild(modal); // escape every ancestor context
modal.hidden = false;
}
Elimina o reubica la propiedad. Andy Blum, de Lullabot, describió un caso en el que un layout de derecha a izquierda reflejaba con un transform el contenedor de un vídeo de fondo, lo que dejaba enterrado el botón de reproducción/pausa del vídeo. Lo solucionó moviendo el transform del contenedor al iframe del vídeo:
/* Before: the wrapper becomes a context */
[dir="rtl"] .hero { transform: scaleX(-1); }
/* After: only the media is mirrored */
[dir="rtl"] .hero iframe { transform: scaleX(-1); }
Añade aislamiento a propósito. Aplicar isolation: isolate en la raíz de un componente crea un contexto de forma deliberada, de modo que todos los z-index dentro del componente se mantienen locales:
.card { isolation: isolate; }
.card .badge { position: absolute; z-index: 1; } /* never competes with the site header */
Prevenlo con una escala de z-index
Un z-index de 9999 casi siempre significa que alguien nunca encontró el contexto de apilamiento, no que necesitara un número mayor. Sustituye los valores puntuales por una escala pequeña y documentada:
:root {
--z-base: 0;
--z-dropdown: 100;
--z-sticky: 200;
--z-overlay: 300;
--z-modal: 400;
--z-toast: 500;
}
.modal { z-index: var(--z-modal); }
Los tokens de la escala de z-index solo ordenan elementos que comparten un contexto de apilamiento. Por eso funcionan mejor combinados con portales para las capas a nivel de página y con isolation: isolate en la raíz de los componentes.
Conclusión
Un z-index que no funciona es casi siempre un problema de contexto de apilamiento, no del número. La próxima vez que 9999 pierda, no toques el valor. Abre el panel Layers o la 3D View para confirmar qué se pinta y dónde, y luego recorre hacia arriba desde ambos elementos con la checklist hasta que las dos cadenas se encuentren. Los dos hijos del ancestro común son los elementos que se están comparando, y la solución debe aplicarse en uno de ellos.
Preguntas frecuentes
¿Por qué z-index no tiene ningún efecto en mi elemento?
z-index solo se aplica a elementos posicionados, es decir, con position definido como relative, absolute, fixed o sticky, y a elementos hijos de flex o grid. En un elemento con el valor por defecto position: static que no sea hijo de flex o grid, el navegador ignora z-index por completo. Añade position: relative para que el valor surta efecto y luego comprueba si algún contexto de apilamiento ancestro sigue limitándolo.
¿Un dialog abierto con showModal() escapa de los contextos de apilamiento?
Sí. Llamar a showModal() sobre un elemento dialog lo promueve al top layer, que se sitúa por encima de todos los contextos de apilamiento del documento. Los transforms y valores de z-index de los ancestros dejan de afectar a su orden. Los popovers mostrados con showPopover() se promueven de la misma manera. z-index no tiene efecto dentro del top layer: los elementos se apilan en el orden en que fueron promovidos, así que el abierto más recientemente aparece encima.
¿Por qué mi overlay con position: fixed se posiciona respecto a su padre en lugar del viewport?
Un ancestro con transform, perspective o filter distinto de none se convierte en el bloque contenedor de los descendientes con posición fija. Un overlay fijo dentro de él se posiciona y dimensiona entonces respecto a ese ancestro, no al viewport. La misma propiedad también crea un contexto de apilamiento, por lo que el overlay puede quedar detrás de otro contenido y cubrir el área equivocada a la vez. Mover el overlay a body soluciona ambos problemas.
¿Puede un z-index negativo colocar a un hijo detrás de su padre?
Solo cuando el padre no crea un contexto de apilamiento. Dentro de un contexto de apilamiento, el navegador pinta primero el fondo y los bordes de la raíz del contexto, después los hijos con z-index negativo y luego el contenido en flujo. Si el padre tiene una opacity inferior a 1, un transform o un z-index distinto de auto con posicionamiento, un hijo con z-index: -1 sigue pintándose por encima del fondo del padre, aunque por debajo de su contenido normal.
Gain control over your UX
See how users are using your site as if you were sitting next to them, learn and iterate faster with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Star on GitHub12k