Cómo funciona sticky por dentro: dos rectángulos y un límite
El modelo mental exacto: el rectángulo del scrollport, el rectángulo del bloque contenedor, y por qué el elemento se despega al llegar al final de su sección.
Un elemento sticky tiene tres comportamientos que parecen distintos —quieto, pegado, arrastrado— y son el resultado de una sola fórmula evaluada en cada fotograma. Entender esa fórmula quita la sensación de magia, explica por qué la cabecera de una sección empuja a la de la siguiente, y da la base para los patrones que veremos después.
- Describir los dos rectángulos que intervienen en el cálculo de un sticky.
- Explicar los tres estados por los que pasa el elemento al hacer scroll.
- Predecir dónde y por qué un sticky deja de estar pegado.
- Aprovechar el límite del contenedor para el patrón de cabeceras que se empujan.
Los dos rectángulos
El motor evalúa, en cada fotograma de scroll, una posición para el elemento sticky. Para calcularla usa dos rectángulos.
El rectángulo de umbral. Es el rectángulo del scrollport, reducido por los desplazamientos que hayas declarado. Si escribes inset-block-start: 1rem, el borde superior del rectángulo de umbral está un remio por debajo del borde superior del scrollport. Es la línea que el elemento no debe cruzar.
El rectángulo del bloque contenedor. Es la caja que delimita hasta dónde puede llegar el elemento. Su bloque contenedor es el de siempre para un elemento en flujo: la caja de contenido de su ancestro más cercano que sea contenedor de bloque o establezca un contexto de formato. Es decir, su padre en la práctica totalidad de los casos.
La regla es: el elemento se coloca en su posición de flujo normal, desplazado lo mínimo necesario para no cruzar el rectángulo de umbral, y sin salir nunca del rectángulo del bloque contenedor. Cuando las dos restricciones entran en conflicto, gana la segunda.
Los tres estados
flowchart TB A[Estado uno, el elemento aun no ha llegado al umbral] --> B[Se comporta como relative sin desplazamiento] B --> C[Sigues bajando] C --> D[Estado dos, el elemento ha cruzado el umbral] D --> E[El motor lo desplaza para mantenerlo en la linea] E --> F[Visualmente parece fixed pero su hueco sigue en el flujo] F --> G[Sigues bajando hasta el final del bloque contenedor] G --> H[Estado tres, el borde del contenedor alcanza al elemento] H --> I[El elemento se va con su contenedor y se despega] style A fill:#89b4fa,color:#11111b style D fill:#a6e3a1,color:#11111b style F fill:#a6e3a1,color:#11111b style H fill:#f9e2af,color:#11111b style I fill:#f9e2af,color:#11111b
Estado uno, antes del umbral. El desplazamiento calculado es cero. El elemento está en su sitio del flujo. Indistinguible de un relative.
Estado dos, pegado. El scroll ha llevado la posición de flujo del elemento por encima de la línea de umbral. El motor le aplica un desplazamiento igual a la diferencia, de modo que visualmente el elemento se queda clavado en la línea. Importante: su hueco en el flujo sigue donde estaba, subiendo con el scroll. Solo el dibujo se queda quieto.
Estado tres, arrastrado. El scroll continúa y el borde final del bloque contenedor alcanza al elemento. A partir de ahí, mantener el umbral obligaría a sacar el elemento de su contenedor, así que la segunda restricción se impone: el elemento deja de estar clavado y se va con el borde del contenedor, saliendo de la pantalla.
Ese tercer estado no es un fallo; es el mecanismo que hace posible el patrón más elegante del posicionamiento pegajoso.
Las cabeceras que se empujan
El comportamiento clásico de una lista de contactos agrupada por letra, o de una tabla de precios por categorías: la cabecera de cada grupo se pega arriba mientras dura su grupo, y cuando llega el siguiente grupo, su cabecera empuja a la anterior hacia arriba hasta sacarla.
Ese efecto no requiere ni una línea de JavaScript. Sale solo del estado tres.
<section class="grupo">
<h3 class="titulo">A</h3>
<ul><li>Ana</li><li>Alberto</li><li>Andrés</li></ul>
</section>
<section class="grupo">
<h3 class="titulo">B</h3>
<ul><li>Berta</li><li>Bruno</li></ul>
</section>
.titulo {
position: sticky;
inset-block-start: 0;
background: Canvas;
padding-block: .5rem;
}
Cada h3 tiene como bloque contenedor su propio section. Mientras el section sigue en pantalla, su título se mantiene pegado arriba. Cuando el section termina de pasar, su borde inferior alcanza al título y se lo lleva. Y como el siguiente section ya ha entrado, su propio título ocupa el sitio en el mismo instante. El efecto de empuje es la consecuencia de que dos elementos comparten la misma línea y sus contenedores se turnan.
Dos detalles que hay que cuidar para que esto se vea bien:
El fondo. Un título sticky sin fondo deja ver el contenido pasando por detrás. background: Canvas usa el color de fondo del sistema y respeta el modo oscuro sin variables.
El apilamiento. Como cada título es sticky, cada uno crea contexto de apilamiento. Si un título tuviera que pasar por encima de otro, necesitarías z-index; en el patrón de empuje no hace falta porque nunca se solapan más que un instante, y el orden de documento ya da el resultado correcto.
El límite en el otro sentido
El mismo mecanismo funciona con inset-block-end y produce el efecto contrario: el elemento se pega al fondo del scrollport hasta que su contenedor lo suelta. Es la geometría de una barra de acciones que acompaña a un formulario largo.
.acciones {
position: sticky;
inset-block-end: 0;
background: Canvas;
padding-block: 1rem;
}
Y declarando los dos, el elemento se pega arriba al bajar y abajo al subir, siempre que haya recorrido para ambas cosas. Es útil para una barra lateral más alta que la ventana: al bajar se queda pegada por abajo hasta que se acaba, y al subir se queda pegada por arriba.
.indice { position: sticky; inset-block: 1rem; align-self: start; }
Cuidado con align-self: start en ese último ejemplo: sin él, en un contenedor grid o flex, la barra se estira y pierde el recorrido, que es la tercera condición de la lección anterior.
La decisión de diseño más importante de sticky —y la que justifica que se añadiera al lenguaje pudiendo resolverse con JavaScript— es que el elemento nunca abandona el flujo. Cuando se pega, su caja de flujo sigue exactamente donde estaba y sigue reservando su espacio; lo único que se desplaza es la pintura. Compáralo con la implementación manual que todos escribimos antes de que existiera: escuchar el evento de scroll, y al cruzar un umbral, cambiar la clase del elemento a una con position: fixed. En el instante del cambio, el elemento sale del flujo, su hueco desaparece, y todo el contenido de debajo salta hacia arriba la altura de la cabecera. La corrección obligatoria era medir la altura del elemento y añadir un padding-top compensatorio al contenedor, lo que a su vez obligaba a medir de nuevo en cada cambio de tamaño de ventana, y a lidiar con que la medición se hacía tras un layout que el propio cambio provocaba. Ese salto, y las tres capas de parches que hacían falta para evitarlo, desaparecen enteros con sticky porque la especificación eligió una definición en la que el problema no puede ocurrir. Es un buen recordatorio de qué distingue una primitiva de la plataforma de una emulación: no es que sea más rápida, es que elimina estados intermedios que la emulación no puede evitar.
- Monta el patrón de cabeceras por grupos y verifica que el empuje ocurre sin JavaScript.
- Dale un borde de color al
sectiony observa el instante exacto en que su borde inferior se lleva al título. - Sustituye
background: Canvaspor nada y comprueba qué se ve por detrás. - Cambia
inset-block-startporinset-block-endy describe cómo cambia el recorrido. - Declara
inset-block: 1remen una barra lateral más alta que la ventana y comprueba que se pega en las dos direcciones.