Posicionamiento y stacking context

Entiende de verdad position sticky, los bugs de z-index y por qué los elementos no se apilan donde esperas. El concepto de stacking context que nadie explica.

El posicionamiento es donde más bugs silenciosos viven en CSS. El z-index que no funciona, el sticky que se queda pegado donde no toca, el elemento que aparece detrás de otro sin razón aparente. Todo tiene una explicación lógica.


Repaso: los tipos de position

position: static;    /* por defecto — flujo normal, no acepta top/left/z-index */
position: relative;  /* en el flujo, pero puedes desplazarlo con top/left */
position: absolute;  /* sale del flujo, se posiciona respecto al ancestro posicionado más cercano */
position: fixed;     /* sale del flujo, se posiciona respecto al viewport */
position: sticky;    /* híbrido: flujo normal hasta alcanzar el threshold, luego se "pega" */

El punto crítico: absolute busca hacia arriba en el DOM hasta encontrar un ancestro con position distinto de static. Si no encuentra ninguno, se posiciona respecto al <html>. Por eso poner position: relative en el contenedor padre es tan habitual.


position: sticky — cómo funciona de verdad

sticky es el más malentendido. No es simplemente “se pega al scroll”. Tiene reglas específicas:

.header {
  position: sticky;
  top: 0;  /* el umbral: a qué distancia del borde se "pega" */
}

Para que funcione correctamente:

  1. Necesita top, bottom, left o right — sin estos valores, se comporta como relative.
  2. El contenedor padre no puede tener overflow: hidden o overflow: auto — esto es el bug más frecuente. Si su padre tiene overflow, el sticky queda confinado y deja de funcionar.
  3. Solo es sticky dentro de su contenedor padre — cuando el padre sale del viewport, el elemento sticky lo sigue. Si el sidebar es sticky pero su contenedor termina antes, el sidebar también termina.
/* Bug frecuente — overflow rompe sticky */
.contenedor {
  overflow: hidden;  /* ← esto mata el sticky de los hijos */
}

.sidebar {
  position: sticky;
  top: 80px;         /* no funciona por el overflow del padre */
}

z-index — el orden de apilamiento

z-index solo funciona en elementos con position distinto de static (o en elementos flex/grid). En static, z-index no tiene efecto, no importa el número que pongas.

/* z-index funciona en: */
.btn { position: relative; z-index: 10; }    /* ✅ */
.modal { position: fixed; z-index: 100; }    /* ✅ */
.item { display: flex; z-index: 5; }         /* ✅ en contexto flex/grid */

/* z-index NO funciona en: */
.texto { z-index: 9999; }  /* ❌ — position: static por defecto */

Stacking context — el concepto que nadie explica

El stacking context (contexto de apilamiento) es el concepto que explica por qué tu z-index: 9999 no sirve de nada a veces. Cuando un elemento crea un stacking context, todos sus hijos viven dentro de ese contexto y no pueden salir de él, independientemente del z-index que tengan.

Es como si cada stacking context fuera una capa de papel: el z-index de los elementos dentro de esa capa solo determina el orden dentro de la capa. La capa completa compite con otras capas por su z-index.

Qué crea un stacking context:

  • position: relative/absolute/fixed/sticky + cualquier z-index que no sea auto
  • opacity menor de 1
  • transform, filter, clip-path, mask
  • will-change con alguna de las propiedades anteriores
  • isolation: isolate (creado específicamente para esto)
  • mix-blend-mode distinto de normal
<div class="capa-A" style="position: relative; z-index: 1">
  <div class="hijo" style="position: relative; z-index: 9999">
    <!-- Este z-index:9999 solo compite DENTRO de capa-A -->
    <!-- Nunca estará por encima de capa-B si capa-B tiene z-index:2 -->
  </div>
</div>

<div class="capa-B" style="position: relative; z-index: 2">
  <!-- capa-B está por encima de capa-A completa, incluyendo sus hijos -->
</div>

isolation: isolate — stacking contexts controlados

Si quieres que un componente tenga su propio contexto de apilamiento interno sin tener que asignar un z-index concreto, usa isolation: isolate:

.modal-wrapper {
  isolation: isolate; /* crea stacking context sin especificar z-index */
}

.modal-wrapper .overlay {
  z-index: 1;          /* estos z-index son locales al modal-wrapper */
}

.modal-wrapper .dialog {
  z-index: 2;
}

Esto evita que los z-indexes internos de un componente interfieran con el resto de la página.


Estrategia de z-index: escala por capas

En vez de asignar valores al azar, define una escala con variables:

:root {
  --z-base:       0;
  --z-dropdown:  10;
  --z-sticky:    20;
  --z-overlay:   30;
  --z-modal:     40;
  --z-toast:     50;
  --z-tooltip:   60;
}

.header {
  position: sticky;
  top: 0;
  z-index: var(--z-sticky);
}

.modal-backdrop {
  position: fixed;
  inset: 0;
  z-index: var(--z-overlay);
}

.modal {
  position: fixed;
  z-index: var(--z-modal);
}

inset — shorthand de top/right/bottom/left

inset es el shorthand moderno para los cuatro valores posicionales:

/* Estos dos bloques son equivalentes */
.overlay {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
}

.overlay {
  position: absolute;
  inset: 0;  /* los cuatro a 0 */
}

/* También funciona con valores distintos */
inset: 1rem;              /* todos iguales */
inset: 1rem 2rem;         /* top/bottom | left/right */
inset: 1rem 2rem 0;       /* top | left/right | bottom */
inset: 1rem 2rem 0 3rem;  /* top | right | bottom | left */

En la siguiente lección dominamos las funciones CSS: calc(), clamp(), min() y max() para crear valores dinámicos y responsive sin necesidad de media queries.