Las tres condiciones que sticky tiene que cumplir
El umbral declarado, el ancestro de scroll y el sitio en el padre: la lista de comprobación que diagnostica un sticky que no se pega en treinta segundos.
position: sticky falla en silencio. No hay error de consola, no hay declaración tachada en DevTools, no hay valor inválido: la propiedad se aplica correctamente y el elemento simplemente no se pega. Eso convierte cada fallo en una investigación a ciegas, salvo que conozcas las tres condiciones que tiene que cumplir. Son solo tres, se comprueban en orden, y cubren la práctica totalidad de los casos.
- Enunciar las tres condiciones de funcionamiento de un elemento sticky.
- Comprobarlas en orden sobre un caso real en menos de un minuto.
- Reconocer el síntoma característico de cada una de las tres.
- Aplicar la corrección correspondiente a cada fallo.
Condición 1: un umbral declarado
position: sticky sin ningún desplazamiento no hace absolutamente nada. El valor inicial de las propiedades de desplazamiento es auto, y un sticky con los cuatro en auto no tiene ningún umbral que cruzar, así que se comporta exactamente como un relative.
/* no hace nada */
.cabecera { position: sticky; }
/* se pega al llegar al borde superior del scrollport */
.cabecera { position: sticky; inset-block-start: 0; }
El valor no tiene que ser cero. inset-block-start: 1rem pega el elemento a un remio del borde, que es lo que quieres si ya hay otra barra ocupando esa franja.
Puedes declarar umbrales en varios bordes a la vez, y cada uno actúa de forma independiente en su dirección: inset-block: 0 hace que el elemento se pegue arriba al bajar y abajo al subir, si es que hay recorrido para las dos cosas.
Síntoma de este fallo: el elemento no se pega nunca, ni un píxel, ni en ninguna dirección. Se comporta exactamente como si tuviera position: relative.
Comprobación en un segundo: mira si hay algún inset, top, bottom, left, right o su equivalente lógico en las reglas que le aplican. Si no lo hay, ese es el problema.
Condición 2: un ancestro de scroll que de verdad haga scroll
Un sticky se pega respecto a su contenedor de scroll más cercano: el ancestro más próximo que establezca un scrollport. Si no hay ninguno, ese contenedor es el viewport.
El fallo aparece cuando el contenedor de scroll más cercano no es el que tú crees. Cualquier ancestro con overflow distinto de visible y de clip —es decir, hidden, auto o scroll, en cualquiera de los dos ejes— crea un contenedor de scroll. Si ese contenedor no tiene contenido que desborde, no hace scroll, y el sticky que hay dentro nunca cruza ningún umbral.
/* este overflow, en cualquier ancestro, secuestra el sticky */
.envoltorio { overflow: hidden; }
El caso más frecuente y más difícil de ver es overflow-x: hidden en html o en body, puesto para evitar el scroll horizontal que provoca algún elemento ancho. La especificación dice que si un eje tiene un valor que no es visible ni clip, el otro eje se computa a auto aunque lo hayas escrito visible. Es decir, overflow-x: hidden convierte el elemento en contenedor de scroll vertical, y a partir de ahí todos los sticky de la página se pegan respecto a él en lugar de respecto al viewport.
Este caso tiene toda una lección propia, porque la corrección es tan precisa como el problema.
Síntoma de este fallo: el elemento no se pega, pero al reducir mucho la ventana o en un contenedor concreto sí funciona. O funciona en una página y no en otra con el mismo componente.
Comprobación en diez segundos: recorre los ancestros buscando cualquier overflow distinto de visible. En la consola:
let n = document.querySelector('.mi-sticky');
while (n && n !== document.documentElement) {
n = n.parentElement;
const o = getComputedStyle(n).overflow;
if (o !== 'visible') console.log(n, o);
}
Condición 3: el padre tiene que dejarle sitio
Un sticky nunca sale de su bloque contenedor. Puede desplazarse dentro de él, pero cuando el borde del contenedor llega, el sticky se va con él. Si el bloque contenedor mide exactamente lo mismo que el sticky, no hay recorrido posible y el elemento parece no pegarse.
Este es el fallo más sutil de los tres porque el CSS del sticky es correcto y el contenedor de scroll también. Lo que falta es espacio.
<!-- el div envoltorio mide justo lo que mide la cabecera: cero recorrido -->
<div class="envoltorio">
<h2 class="cabecera">Sección</h2>
</div>
<div class="contenido">...</div>
La corrección es estructural: el sticky y el contenido por el que debe deslizarse tienen que ser hermanos dentro del mismo padre.
<section>
<h2 class="cabecera">Sección</h2>
<div class="contenido">...</div>
</section>
Hay una variante de este fallo que confunde especialmente porque el padre sí es alto: cuando el padre es un contenedor flex o grid y el sticky es uno de sus hijos. El valor inicial de align-items es stretch, así que el hijo se estira hasta ocupar todo el alto de la fila y vuelve a no tener recorrido. La corrección es una línea:
.barra-lateral { position: sticky; inset-block-start: 1rem; align-self: start; }
Síntoma de este fallo: el elemento no se pega, o se pega y se despega enseguida al llegar al final de una sección corta.
Comprobación en veinte segundos: dale un fondo llamativo al padre del sticky y mira si es más alto que el sticky. Si no lo es, ahí está el problema.
flowchart TB
A[Mi sticky no se pega] --> B{Hay algun inset declarado}
B -->|No| C[Declara inset-block-start o el borde que corresponda]
B -->|Si| D{Algun ancestro tiene overflow distinto de visible}
D -->|Si| E[Ese es el contenedor de scroll y no hace scroll]
E --> F[Cambialo a clip o quitalo]
D -->|No| G{El padre es mas alto que el sticky}
G -->|No| H{El padre es flex o grid}
H -->|Si| I[align-self start para que no se estire]
H -->|No| J[Reestructura para que el sticky y el contenido sean hermanos]
G -->|Si| K[Las tres condiciones se cumplen, revisa z-index y recorte]
style A fill:#f38ba8,color:#11111b
style C fill:#a6e3a1,color:#11111b
style F fill:#a6e3a1,color:#11111b
style I fill:#a6e3a1,color:#11111b
style J fill:#a6e3a1,color:#11111b
style K fill:#89b4fa,color:#11111bLa lista de comprobación de treinta segundos
Cuando un sticky no funcione, ejecuta esto en orden y para en cuanto encuentres algo:
- ¿Tiene declarado al menos un desplazamiento distinto de
auto? Si no, ese es el fallo. - ¿Algún ancestro tiene
overflowdistinto devisibley declip? Si sí, ese es el fallo. - ¿El padre del sticky es más alto que el sticky? Si no, ese es el fallo.
- Si las tres se cumplen y aun así no lo ves, el elemento sí está pegado y lo que tienes es otro bug: o queda por debajo de algo por apilamiento, o lo está recortando un
overflowque sí desborda.
Ese cuarto punto separa dos bugs que se confunden todo el tiempo. Un sticky que funciona pero queda tapado no es un problema de sticky: es el problema de apilamiento que trata la lección sobre por qué tu z-index de 9999 sigue quedando debajo, agravado por el hecho de que position: sticky crea contexto de apilamiento por sí solo.
La forma más productiva de pensar en sticky es dejar de verlo como un sexto valor de posición y verlo como lo que la especificación describe: un elemento relativamente posicionado cuyo desplazamiento no lo eliges tú, lo calcula el motor en cada fotograma para satisfacer una restricción. La restricción dice: mantén el borde declarado del elemento a esta distancia del borde del scrollport, siempre que puedas hacerlo sin sacar el elemento de su bloque contenedor. De ahí salen las tres condiciones sin tener que memorizarlas. Sin umbral no hay restricción que satisfacer, así que el desplazamiento calculado es cero y el elemento se queda en su sitio. Sin scrollport que se mueva, la restricción se cumple trivialmente todo el rato y el desplazamiento vuelve a ser cero. Y sin espacio en el bloque contenedor, la restricción entra en conflicto con el límite y gana el límite. Esta lectura también explica el comportamiento que más desconcierta a quien viene de resolver esto con JavaScript: el sticky nunca sale de su hueco original en el flujo, porque sigue siendo un relative y los relative no perturban el layout. Por eso no hay salto de contenido cuando se pega, algo que ninguna implementación manual con scroll y position: fixed consigue sin medir y compensar a mano.
- Escribe un
position: stickysin desplazamientos y comprueba que se comporta igual querelative. - Añade
overflow: hiddena un ancestro cualquiera y observa cómo el sticky deja de pegarse. - Envuelve el sticky en un div que mida exactamente lo mismo que él y verifica que no tiene recorrido.
- Mete el sticky como hijo de un contenedor flex sin
align-selfy comprueba que se estira. - Ejecuta el bucle de consola de la condición 2 sobre un caso real y anota todos los ancestros con overflow.