z-index no funciona: 5 soluciones definitivas

“Le puse z-index: 9999 y sigue detrás” 😤 Si esto te suena familiar, este artículo es para ti.

El problema de z-index no suele ser el valor que asignas, sino el contexto de apilamiento que estás ignorando.


La causa raíz: stacking context

Cada elemento HTML crea su propio contexto de apilamiento. Los elementos hijos solo pueden estar dentro de ese contexto, nunca salir de él.

Esto significa:

  • Aunque tu hijo tenga z-index: 9999
  • Si su padre tiene z-index: 1
  • El hijo nunca podrá estar encima de elementos externos al padre

Las 5 soluciones

Solución 1: Añade position al elemento

z-index solo funciona con elementos posicionados.

/* ❌ NO funciona */
.boton-flotante {
  z-index: 100;
}

/* ✅ CORRECTO */
.boton-flotante {
  position: relative;
  z-index: 100;
}

Valores válidos de position: relative, absolute, fixed, sticky


Solución 2: Saca al hijo del contexto del padre

Si el padre está limitando a tu hijo, tienes opciones:

Opción A: Mueve el HTML

<!-- ❌ El dropdown está dentro de .header -->
<div class="header">
  <div class="dropdown">...</div>
</div>

<!-- ✅ El dropdown está fuera -->
<div class="header"></div>
<div class="dropdown">...</div>

Opción B: Usa Portal (React/Vue)

// React - Renderiza fuera del padre
import { createPortal } from 'react-dom';

return createPortal(
  <Dropdown />,
  document.body
);

Solución 3: Remueve propiedades que crean nuevo contexto

Estas propiedades crean stacking context automáticamente:

  • opacity menor a 1
  • transform (cualquier valor excepto none)
  • filter
  • isolation: isolate
  • position: fixed
/* ❌ Crea nuevo contexto */
.caja {
  opacity: 0.9;
  transform: scale(1.1);
}

/* ✅ Alternativas sin crear contexto */
.caja {
  background: rgba(255, 255, 255, 0.9);
  /* transform eliminado */
}

Solución 4: Usa position: fixed para overlays

Para modales y popups, position: fixed es más confiable:

.modal-overlay {
  position: fixed;
  z-index: 9999;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.5);
}

.modal-contenido {
  position: relative;
  z-index: 10000; /* Mayor que el overlay */
}

Solución 5: Establece el z-index del padre

A veces, solo necesitas darle al padre el z-index correcto:

.header {
  position: relative;
  z-index: 100;
}

.dropdown {
  position: absolute;
  z-index: 200; /* Ahora funciona */
  top: 100%;
}

Ejemplo completo: Modal que no aparece

El problema

<div class="modal-container"> <!-- z-index: 1 -->
  <div class="modal"> <!-- z-index: 9999 - NO FUNCIONA -->
    Contenido
  </div>
</div>

La solución

<div class="modal-container">
  <!-- El modal se rendered en document.body via portal -->
</div>

<!-- O directamente: -->
<div class="modal">
  Contenido
</div>
.modal {
  position: fixed; /* Crea contexto independiente */
  z-index: 9999;
  /* Centrado */
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

Tabla de referencias rápidas

ProblemaSolución
z-index no tiene efectoAñadir position: relative/absolute/fixed/sticky
Hijo detrás de elementos externosRemover el padre del flujo o usar portal
z-index no supera a otrosVerificar z-index del padre
Elementos se cortanRevisar overflow: hidden del padre
Modal detrás de headerUsar position: fixed en el modal
Dropdown detrás del contenidoSubir el z-index del navbar o usar portal
Backdrop sobre el contenido del modalAñadir z-index: +1 al contenido vs backdrop

Problemas específicos: dropdowns

¿Por qué el dropdown queda atrás?

El dropdown está limitado por el z-index de su padre (navbar o header), aunque uses z-index: 9999 dentro:

<nav class="navbar" style="z-index: 10">
  <div class="menu-container">
    <button>Menú</button>
    <div class="dropdown">
      <!-- Este dropdown NO puede salir del navbar -->
      Opciones...
    </div>
  </div>
</nav>

<main style="z-index: 1">
  <div class="card">Producto 1</div>
  <div class="card">Producto 2</div>
</main>

Soluciones para dropdowns

/* Solución 1: sube el z-index del padre */
.navbar {
  position: relative;
  z-index: 1000;
}
.dropdown {
  position: absolute;
  z-index: 1001; /* Más alto que el navbar */
}
/* Solución 2: position: fixed para menús flotantes.
   Precaución: puede dar problemas con scroll en páginas largas. */
.dropdown {
  position: fixed;
  z-index: 9999;
  top: 100%;
  left: 0;
}
// Solución 3: portal para renderizar el dropdown en document.body
import { createPortal } from 'react-dom';

function Dropdown({ isOpen, children }) {
  if (!isOpen) return null;
  return createPortal(
    <div className="dropdown">{children}</div>,
    document.body
  );
}

Sticky header + dropdown

Si tienes un header sticky, el dropdown debe tener un z-index mayor que el header:

.sticky-header {
  position: sticky;
  z-index: 100;
}
.dropdown {
  z-index: 200; /* Debe ser mayor que el header */
}

Checklist para dropdowns

  • ¿El navbar/header tiene z-index?
  • ¿El dropdown tiene mayor z-index que su padre?
  • ¿Hay overflow: hidden cortando el dropdown?
  • ¿Usaste position: fixed o un portal si es necesario?

Problemas específicos: modales

¿Por qué el modal queda detrás del header?

El modal está limitado por el stacking context de .app-container, incluso con z-index: 9999:

<body>
  <header style="z-index: 100"></header>

  <div class="app-container" style="z-index: 1">
    <button>Abrir Modal</button>
  </div>

  <div class="modal" style="z-index: 9999">
    <!-- PERO APARECE DETRÁS DEL HEADER -->
  </div>
</body>

Soluciones para modales

/* Solución 1: position: fixed crea su propio stacking context */
.modal {
  position: fixed;
  z-index: 9999;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}
// Solución 2: portal en React
import { createPortal } from 'react-dom';

function Modal({ children }) {
  return createPortal(
    <div className="modal-overlay">{children}</div>,
    document.body
  );
}
<!-- Solución 2b: teleport en Vue 3 -->
<teleport to="body">
  <div class="modal">Contenido</div>
</teleport>
// Solución 2c: mover el HTML a body con vanilla JS
function openModal(htmlContent) {
  const modal = document.createElement('div');
  modal.className = 'modal';
  modal.innerHTML = htmlContent;
  document.body.appendChild(modal);
}
<!-- Solución 3: mueve el HTML fuera del padre -->
<main class="app">
  <button onclick="openModal()">Abrir</button>
</main>
<div class="modal">...</div>   <!-- ✅ fuera del app container -->

Backdrop detrás del modal

El contenido del modal debe tener un z-index mayor que su backdrop:

.modal-backdrop {
  position: fixed;
  z-index: 9998;
}
.modal-content {
  position: relative;
  z-index: 9999; /* Mayor que backdrop - CORRECTO */
}

Checklist para modales

  • ¿El modal tiene position: fixed?
  • ¿Está renderizado en document.body o nivel superior?
  • ¿El contenido del modal tiene mayor z-index que el overlay?
  • ¿Algún padre tiene opacity o transform?

Problemas específicos: overflow: hidden

¿Por qué overflow:hidden corta elementos?

overflow: hidden actúa como una barrera que recorta todo lo que sale de los límites del contenedor, incluyendo elementos posicionados:

.card-container {
  overflow: hidden;
}
.badge {
  position: absolute;
  top: -10px;
  right: -10px;
}
/* El badge aparece cortado */

Las soluciones

/* Solución 1: usa overflow: visible (la más simple) */
.card {
  overflow: visible; /* Permite que el badge se vea */
}
/* Solución 2: expande el contenedor y da espacio al elemento */
.card {
  overflow: hidden;
  padding: 10px; /* Espacio para el elemento posicionado */
}
<!-- Solución 3: mueve el elemento fuera del contenedor cortado -->
<div class="card-wrapper" style="position: relative">
  <div class="card" style="overflow: hidden">
    <img src="producto.jpg" />
  </div>
  <span class="badge">Oferta</span>  <!-- ✅ fuera del contenedor -->
</div>
/* Solución 4: cuidado con overflow:hidden en flex/grid containers */
.flex-container {
  display: flex;
  overflow: visible; /* En vez de hidden */
}

Errores comunes con overflow

/* ❌ Pensar que z-index lo arreglará */
.badge {
  position: absolute;
  z-index: 9999; /* Ignorado si el padre tiene overflow:hidden */
}

/* ❌ Aplicar overflow:hidden a todo */
* { overflow: hidden; } /* Ahórrate el "too much clipping" */

Checklist para overflow

  • ¿El padre tiene overflow: hidden?
  • ¿Puedes usar overflow: visible?
  • ¿Puedes mover el elemento fuera del padre?
  • ¿Expandir el padding del padre ayuda?

Errores comunes con z-index

❌ z-index: 999999

Poner números excesivamente altos no ayuda. Usa una escala lógica:

.dropdown    { z-index: 10; }
.sticky-nav  { z-index: 100; }
.modal       { z-index: 1000; }
.toast       { z-index: 2000; }

❌ Ignorar el orden de apilamiento por defecto

El navegador apila elementos en un orden específico incluso sin z-index:

  1. Elementos con position: static (fondo de la página)
  2. Elementos flotantes
  3. Elementos con position: static en línea
  4. Elementos posicionados (aquí entra z-index)

❌ Mezclar unidades de z-index

Todos los elementos deberían usar la misma escala para evitar confusiones.


Guía paso a paso para debuggear z-index

  1. Verifica que tenga position: asegúrate de position: relative, absolute, fixed o sticky.
  2. Inspecciona el padre: el padre puede estar limitando el z-index del hijo.
  3. Busca propiedades problemáticas: revisa opacity, transform, filter en los padres.
  4. Usa DevTools: en Chrome activa el panel “Layers” para visualizar las capas.
  5. Prueba con valores extremos: si z-index: 1 no funciona, el problema no es el valor.

Artículos relacionados


: El número en z-index no es la respuesta. El contexto lo es.

Referencias