wandres.dev
STICKY · Cómo funciona de verdad

El patrón de cabecera pegajosa, y las cabeceras apiladas

Cómo montar una barra superior que funcione de verdad: apilamiento, altura estable, cabeceras encadenadas y tablas con cabecera y primera columna fijas.

⏱ 18 min

La cabecera pegajosa es el patrón más pedido y el que más detalles tiene: hay que resolver el apilamiento, evitar que la altura cambie al pegarse, encadenar dos barras que se pegan a distintas alturas, y hacer que una tabla larga mantenga a la vez su fila de cabecera y su primera columna. Ninguno de esos problemas necesita JavaScript, pero todos necesitan saber exactamente qué se está declarando.

🎯 Al terminar esta lección sabrás
  • Montar una cabecera pegajosa con apilamiento correcto.
  • Encadenar dos elementos pegajosos a distintas alturas.
  • Construir una tabla con cabecera y primera columna fijas.
  • Evitar los cambios de altura que provocan saltos de contenido.

La cabecera básica, bien hecha

.cabecera {
  position: sticky;
  inset-block-start: 0;
  z-index: 2;
  background: Canvas;
  border-block-end: 1px solid color-mix(in oklch, CanvasText 15%, transparent);
}

Cuatro decisiones y ninguna es decorativa.

inset-block-start: 0 es el umbral. Sin él no hay pegado.

z-index: 2 es necesario porque, aunque position: sticky crea contexto de apilamiento por sí solo, ese contexto compite con los de sus hermanos por el paso 6 del orden de pintado, es decir, por orden de documento. Una cabecera que va antes que el contenido perdería contra cualquier hermano posterior que también forme contexto. El número tiene que ser mayor que el de los demás elementos del mismo contexto, y en una escala sana eso significa 2 o 3, no 9999.

background es obligatorio. Sin fondo, el contenido se ve pasar por debajo del texto de la cabecera. Canvas y CanvasText son colores del sistema que se adaptan solos al modo claro y oscuro cuando color-scheme está declarado.

El borde inferior no es estético: marca la frontera entre la barra y el contenido que pasa por detrás, y sin él la superposición se lee mal cuando el contenido tiene un fondo del mismo color.

Un error frecuente que conviene señalar: envolver la cabecera en un div para “agruparla”. Si ese div es su padre y mide justo lo que mide la cabecera, la tercera condición falla y el pegado desaparece. La cabecera debe ser hermana del contenido por el que se desliza.

Dos barras encadenadas

El caso de una barra principal siempre visible y, debajo, una barra de filtros que se pega justo por debajo de ella. La geometría se resuelve con umbrales distintos.

:root { --alto-barra: 3.5rem; }

.barra-principal {
  position: sticky;
  inset-block-start: 0;
  block-size: var(--alto-barra);
  z-index: 3;
}

.barra-filtros {
  position: sticky;
  inset-block-start: var(--alto-barra);   /* justo debajo de la anterior */
  z-index: 2;
}

La segunda barra se pega a la altura donde acaba la primera. El z-index decreciente garantiza que la principal pase por encima si en algún momento se solapan.

La fragilidad de este patrón es evidente: el --alto-barra tiene que coincidir con la altura real. Si la barra principal crece —porque el logotipo cambia, porque el texto se envuelve en móvil, porque el usuario aumenta el tamaño de fuente— el encadenamiento se descuadra. Dos formas de robustecerlo:

Fijar la altura de verdad, declarando block-size en la barra principal en lugar de dejar que crezca con el contenido. Es lo que hace el ejemplo, y es la opción sencilla.

Medir y publicar la altura con un ResizeObserver que escriba la custom property. Cuesta seis líneas y elimina el acoplamiento.

const barra = document.querySelector('.barra-principal');
new ResizeObserver(([e]) => {
  document.documentElement.style.setProperty(
    '--alto-barra', e.contentRect.height + 'px'
  );
}).observe(barra);

La tabla con cabecera y primera columna fijas

position: sticky funciona sobre elementos de tabla en los cuatro motores desde hace años: Chrome 56, Firefox 59 y Safari 8 lo soportan sobre thead, tr, th y td.

.tabla-envoltorio { overflow: auto; max-block-size: 70vh; }

table { border-collapse: separate; border-spacing: 0; }

thead th {
  position: sticky;
  inset-block-start: 0;
  z-index: 2;
  background: Canvas;
}

tbody th {
  position: sticky;
  inset-inline-start: 0;
  z-index: 1;
  background: Canvas;
}

thead th:first-child {
  z-index: 3;          /* la esquina: fija en los dos ejes */
  inset-inline-start: 0;
}

Tres niveles de apilamiento porque hay tres casos: la fila de cabecera pasa por encima del cuerpo, la primera columna también, y la celda de la esquina tiene que pasar por encima de las dos.

Fíjate en el border-collapse: separate. Con collapse, los bordes pertenecen a la tabla y no a las celdas, y al desplazarse las celdas pegadas los bordes se quedan atrás: el resultado es una cabecera sin líneas. La solución robusta es usar separate con border-spacing: 0 y dibujar las líneas con box-shadow o con el borde de la propia celda.

thead th { box-shadow: inset 0 -1px 0 color-mix(in oklch, CanvasText 20%, transparent); }

Y fíjate también en que el contenedor de scroll es .tabla-envoltorio con overflow: auto. Eso es deliberado: aquí sí queremos que el sticky se pegue respecto a ese contenedor y no respecto al viewport, porque la tabla debe desplazarse dentro de su caja. Es el mismo mecanismo que rompía el sticky en la lección anterior, usado a propósito.

flowchart TB
A[Necesitas algo pegado arriba] --> B{Se pega respecto a la ventana o a un panel}
B -->|A la ventana| C[Ningun ancestro debe tener overflow que cree scroll]
B -->|A un panel| D[El panel lleva overflow auto y es el contenedor de scroll]
C --> E{Hay mas de una barra}
D --> E
E -->|Una| F[inset-block-start cero mas z-index dos mas fondo]
E -->|Dos o mas| G[Umbrales acumulados y z-index decreciente]
G --> H{La altura de la primera es estable}
H -->|Si| I[Custom property con el valor fijo]
H -->|No| J[ResizeObserver que publica la altura medida]
style A fill:#89b4fa,color:#11111b
style F fill:#a6e3a1,color:#11111b
style I fill:#a6e3a1,color:#11111b
style J fill:#a6e3a1,color:#11111b
style H fill:#f9e2af,color:#11111b

El detalle de la altura que cambia

Un patrón muy visto: la cabecera se encoge o cambia de estilo al pegarse. Si ese cambio altera su altura, provoca un salto en el contenido, porque el hueco que la cabecera ocupa en el flujo cambia de tamaño en mitad del scroll y todo lo de debajo se recoloca.

La regla es: al pegarse, cambia lo que quieras menos la altura. El fondo, la sombra, la opacidad de un subtítulo, el tamaño de un logotipo dentro de una caja de altura fija: todo eso es gratis. Reducir el padding o el block-size de la propia barra no lo es.

Si el diseño exige que la barra encoja, la forma de hacerlo sin salto es que el elemento sticky tenga altura fija y el encogimiento ocurra dentro de él:

.cabecera { block-size: 4rem; }               /* nunca cambia */
.cabecera .logo { block-size: 3rem; transition: block-size .2s; }

Y el cambio de estado se detecta con las técnicas de la siguiente lección.

Cada elemento pegajoso es un contexto de apilamiento, y eso convierte la escala de z-index en local

Aquí hay una consecuencia que se pasa por alto y que cambia cómo escribes los números. position: sticky crea contexto de apilamiento siempre, con o sin z-index. Eso significa que una cabecera pegajosa es una caja cerrada: cualquier menú, tooltip o desplegable que cuelgue de ella tiene su z-index acotado dentro de la cabecera y no puede pasar por encima de nada que esté fuera. El síntoma en producción es característico y desconcierta mucho: el menú de usuario de la barra superior queda por debajo de un modal, o por debajo de una segunda barra, y subir su número no hace nada porque el número que compite es el de la cabecera. Hay dos salidas y ninguna pasa por tocar el desplegable. La primera es subir el z-index de la cabecera, que es el que realmente participa en la comparación. La segunda, y la correcta cuando el elemento debe flotar sobre toda la página, es sacarlo a la capa superior con popover, que es inmune a todo esto. Como regla operativa: si un componente contiene elementos que deben poder salir de él, no lo hagas pegajoso; haz pegajoso a un envoltorio que no los contenga, o usa la capa superior para lo que tiene que salir.

⚔️ Monta las cuatro variantes
  1. Construye una cabecera pegajosa básica y verifica que el contenido no se ve por detrás del texto.
  2. Encadena dos barras con umbrales acumulados y comprueba el orden de apilamiento entre ellas.
  3. Añade el ResizeObserver y confirma que el encadenamiento sobrevive a un cambio de tamaño de fuente.
  4. Monta la tabla con cabecera y primera columna fijas, incluida la esquina, y comprueba los tres niveles de z-index.
  5. Cambia el padding de la cabecera al pegarse y observa el salto. Corrígelo fijando la altura y animando el contenido.