wandres.dev
LA CAPA SUPERIOR · dialog, popover y el top layer

La capa superior y el final del z-index

Qué es el top layer, cómo se entra y se sale de él, por qué hace irrelevante el z-index para lo que está dentro, y qué es exactamente ::backdrop.

⏱ 17 min

La capa superior no es un z-index muy alto. Es un contenedor de renderizado que existe fuera del árbol de pintado del documento, definido en la propia especificación de CSS de posicionamiento, y en el que el navegador coloca ciertos elementos por orden de llegada. Un elemento que está ahí se pinta por encima de absolutamente todo lo demás, sin depender de ningún contexto de apilamiento ni de ningún bloque contenedor intermedio. Es la respuesta de la plataforma a dos décadas de guerras de números.

🎯 Al terminar esta lección sabrás
  • Describir qué es la capa superior y cómo se ordena su contenido.
  • Enumerar las tres formas de entrar en ella.
  • Explicar por qué el z-index no tiene efecto entre sus elementos.
  • Estilar el fondo de un elemento de la capa superior con ::backdrop.

Qué es exactamente

Cada documento tiene, además de su árbol de cajas normal, una capa superior: una pila de elementos que se pintan después de todo el contenido del documento, en el orden en que entraron. El último que entra queda por encima.

Un elemento promocionado a la capa superior sigue estando en el DOM donde lo pusiste. Hereda los estilos de sus antepasados, participa en el orden de lectura del documento, sus selectores siguen funcionando. Lo único que cambia es dónde se pinta: sale del árbol de pintado normal y pasa a la pila.

Esa separación es la que resuelve el problema. Todo lo que la lección sobre por qué tu z-index de 9999 sigue quedando debajo diagnostica —contextos de apilamiento que encierran, bloques contenedores que roban el viewport, overflow que recorta— ocurre dentro del árbol de pintado. Un elemento que no está ahí no puede sufrirlo.

Hay tres formas de entrar:

  1. Un dialog abierto con showModal().
  2. Un elemento con el atributo popover mostrado.
  3. Un elemento en pantalla completa, vía requestFullscreen().

Y ninguna otra. No hay una propiedad CSS que promocione un elemento arbitrario a la capa superior; la entrada la controla siempre el HTML o una API del DOM. Esa restricción es deliberada: si fuera una propiedad de estilo, cualquier hoja de terceros podría promocionar cualquier cosa y volveríamos al mismo problema con otro nombre.

Por qué el z-index deja de importar

Dentro de la capa superior, el orden lo determina exclusivamente el momento de entrada. z-index no se ignora formalmente —cada elemento de la capa superior forma su propio contexto de apilamiento, y dentro de él el z-index de sus descendientes funciona con normalidad— pero no sirve para ordenar dos elementos de la capa superior entre sí.

<dialog id="a" style="z-index: 1">Abro primero</dialog>
<dialog id="b" style="z-index: 999999">Abro después</dialog>
a.showModal();
b.showModal();   // queda por encima de a, pese al z-index

Si cierras b y lo vuelves a abrir, vuelve a quedar arriba. El orden es una pila y se comporta como una pila.

Esto tiene una consecuencia práctica excelente: el orden de apilamiento de tus superposiciones deja de ser un problema de mantenimiento. Un modal abierto desde otro modal queda encima porque se abrió después, no porque alguien recordara subirle el número. Un menú abierto desde dentro de un modal queda encima del modal por la misma razón.

flowchart TB
A[Arbol de pintado del documento] --> B[Contextos de apilamiento anidados]
B --> C[Aqui z-index ordena y los contextos encierran]
D[Capa superior] --> E[Pila ordenada por momento de entrada]
E --> F[Aqui z-index no ordena entre elementos]
G[El navegador pinta primero el documento] --> A
G --> H[Y despues la capa superior encima de todo]
H --> D
style A fill:#89b4fa,color:#11111b
style C fill:#f9e2af,color:#11111b
style D fill:#a6e3a1,color:#11111b
style F fill:#a6e3a1,color:#11111b
style H fill:#cba6f7,color:#11111b

Un matiz que conviene tener claro: la promoción a la capa superior no cambia el bloque contenedor del elemento por sí sola. Lo que ocurre en la práctica es que las hojas de estilo de usuario del navegador dan a dialog y a los elementos con popover un position: fixed cuando están abiertos, y eso sí los ancla al viewport. Si sobrescribes esa posición con position: absolute, el elemento vuelve a depender de su bloque contenedor para la geometría, aunque siga pintándose encima de todo. Es exactamente lo que quieres cuando lo anclas a otro elemento, y es la base del patrón de la última lección de este nivel.

::backdrop

::backdrop es un pseudo-elemento que existe únicamente para los elementos de la capa superior. Es una caja del tamaño del viewport que se pinta justo por debajo del elemento y por encima de todo lo demás.

dialog::backdrop {
  background: oklch(0% 0 0 / .5);
  backdrop-filter: blur(2px);
}

Cuatro cosas que hay que saber de él y que casi nunca se cuentan juntas:

Hereda del elemento que lo origina, pero eso es reciente. Durante años ::backdrop no heredaba de nada: era un huérfano en el árbol de herencia, y las custom properties de tu sistema de diseño no llegaban a él. Escribir background: var(--velo) no resolvía. La especificación se corrigió y los motores lo implementaron entre finales de 2023 y principios de 2024: Firefox 120, Chrome 122 y Safari 17.4. Desde entonces ::backdrop hereda de su elemento originador y las variables sí llegan. Si tienes que soportar navegadores anteriores, declara los valores literalmente en la regla del pseudo-elemento.

Solo existe si el elemento está en la capa superior. Un dialog abierto con show() en lugar de showModal() no genera ::backdrop, porque no entra en la capa superior. Un popover abierto sí lo genera, aunque su estilo por defecto sea transparente.

Hay uno por elemento, no uno global. Con dos modales abiertos hay dos ::backdrop apilados. Si los dos son semitransparentes, el oscurecimiento se acumula. Suele ser lo que quieres, pero conviene saberlo.

Es animable como cualquier otro elemento. Puedes darle una transición de opacidad, y con @starting-style también animar su entrada, que es el tema de la quinta lección.

Un valor por defecto que conviene conocer: el ::backdrop de un dialog modal viene con un fondo semitransparente en la hoja de usuario, y el de un popover viene transparente. Por eso un popover no oscurece la página y un modal sí.

La capa superior es lo que hace que el foco y el apilamiento por fin coincidan

El valor de la capa superior que menos se menciona, y que es el que de verdad importa, es la accesibilidad. Piensa en cómo se construía un modal antes: un div con position: fixed y z-index alto, montado en un portal como hijo de body. Ese div está visualmente por encima de todo, pero para el resto de la plataforma no es especial en absoluto. El contenido de detrás sigue siendo enfocable con tabulador, sigue siendo leído por los lectores de pantalla, sigue respondiendo a los clics si tu velo no los intercepta bien. Toda la corrección tenía que escribirse a mano: capturar el foco con un ciclo de tabulación, poner inert o aria-hidden en el resto del documento y acordarse de quitarlo, escuchar la tecla de escape, guardar el elemento que tenía el foco antes para devolvérselo al cerrar, impedir el scroll de fondo. Cinco mecanismos independientes que había que mantener sincronizados con el estado visual, y que en la práctica se implementaban mal o a medias en la mayoría de los sitios. La capa superior acopla las dos cosas por definición: showModal() promociona, aísla el resto del documento con inercia, mueve el foco, atiende la tecla de escape y devuelve el foco al cerrar, todo como una sola operación atómica que no se puede desincronizar. La conclusión no es “ahora es más cómodo”: es que el estado visual y el estado de interacción dejaron de ser dos cosas que un desarrollador podía dejar en desacuerdo, y esa clase entera de fallos de accesibilidad desapareció.

⚔️ Explora la pila
  1. Abre dos dialog modales seguidos con z-index invertidos y comprueba cuál queda encima.
  2. Cierra el segundo y vuelve a abrirlo; verifica que vuelve a la cima de la pila.
  3. Abre un dialog con show() y comprueba que no genera ::backdrop.
  4. Declara background: var(--velo) en un ::backdrop con la variable definida en el propio dialog y comprueba que hereda.
  5. Anida un modal dentro de un contenedor con transform y confirma que sigue cubriendo la pantalla entera.