Elementos sticky sin dolor: barras laterales, cabeceras y por qué fallan

¿Te fue útil?

La interfaz sticky es una característica que solo notas cuando falla. Una cabecera que desaparece a mitad del desplazamiento, una barra lateral que se niega a fijarse, un botón “sticky” que se adhiere a lo incorrecto—normalmente justo antes de un lanzamiento, durante una demo o mientras tu CEO “solo revisa rápidamente el sitio en su iPad”.

CSS hizo que sticky pareciera engañosamente sencillo, y luego los sistemas en producción lo volvieron honesto. Sticky depende de contenedores de desplazamiento, contención, apilamiento, modos de diseño y peculiaridades del navegador. Trátalo como un problema de fiabilidad: define el comportamiento deseado, observa la cadena de desplazamiento real y luego elimina las trampas.

Un modelo mental de producción para sticky

position: sticky no es ni fixed ni relative. Es un híbrido que se comporta como relative hasta un umbral, luego se comporta como “fijo dentro de un límite”, y deja de ser sticky cuando alcanza el final de ese límite.

Las tres reglas que importan más que nada

  1. Sticky se ancla al contenedor de desplazamiento más cercano (más precisamente: el ancestro más cercano que crea una caja de desplazamiento y recorta overflow para ese eje). Si piensas “la ventana” (viewport), ya estás en problemas.
  2. Sticky necesita un umbral (top, bottom, left o right) y ese umbral debe tener sentido en el eje en el que te desplazas.
  3. Sticky está limitado por su bloque contenedor. No flotará por encima del final del contenedor solo porque se lo pidas amablemente.

El informe de error típico dice: “funcionaba ayer”. ¿Qué cambió ayer? No sticky. La cadena de ancestros. Alguien envolvió el contenido en un nuevo div con overflow: hidden, añadió transform para una animación elegante, o cambió el diseño a flex/grid y cambió accidentalmente el comportamiento de min-size. Sticky no cambió. Tu contención sí.

Define el comportamiento como un SRE: ¿qué es “correcto”?

Escribe explícitamente:

  • Qué elemento debe pegarse (y en qué eje).
  • Dónde debe pegarse (top: 0, top: 64px debajo de una navegación global, etc.).
  • Respecto a qué debe pegarse (viewport vs sección de página vs cuerpo de modal).
  • Cuándo debe dejar de pegarse (final de la columna del artículo, final de la región de la barra lateral, fondo del modal).

Esto no es teatro de procesos. Es cómo evitas la situación más común de “sticky contra sticky”: una cabecera global está sticky respecto al viewport, una cabecera local está sticky respecto a un desplazador interno, y ambas reclaman los mismos píxeles. El navegador elegirá; a tu equipo de producto no le gustará la elección.

Sticky y la cadena de desplazamiento: encuentra el scroller que realmente estás usando

Muchas aplicaciones ya no desplazan el documento. Desplazan un div raíz (#app), un contenedor de layout (.shell) o el cuerpo de un modal. Está bien—pero sticky usará ese scroller, no el viewport, si las propiedades de overflow crean un contenedor de desplazamiento.

Si recuerdas solo un principio de diagnóstico, que sea este: sticky no falla al azar; falla de forma determinista porque el contenedor de desplazamiento y el bloque contenedor no son los que crees.

Broma 1/2: Sticky es como un gato—si intentas forzarlo, deja de cooperar y te mira hasta que cambias el entorno.

Una cita, porque operaciones guarda recibos

La esperanza no es una estrategia. — idea parafraseada comúnmente atribuida a ingenieros y operadores en círculos de fiabilidad.

Hechos e historia que explican las rarezas actuales

La UI sticky parece moderna, pero las restricciones son antiguas. El motor de maquetación del navegador ha estado negociando entre “pinta esto aquí” y “no rompas el desplazamiento” durante décadas.

  • Hecho 1: El comportamiento sticky existió como una extensión de WebKit (-webkit-sticky) antes de estandarizarse, por eso las peculiaridades heredadas de Safari aún se notan en casos límite.
  • Hecho 2: Las primeras “cabeceras sticky” eran a menudo handlers de scroll en JavaScript que actualizaban position: fixed, lo que causaba jank porque los eventos de scroll se disparan mucho y el layout sufre si lees/escribes el DOM repetidamente.
  • Hecho 3: El auge de las single-page apps movió el desplazamiento a contenedores anidados para mantener la “app shell” estática—creando accidentalmente la razón número 1 por la que sticky “deja de funcionar”.
  • Hecho 4: overflow: hidden se convirtió en un clearfix por defecto y un hack para “prevenir rebotes” en muchas bases de código. También recorta y establece comportamiento de scroll/contención de maneras que interfieren con sticky.
  • Hecho 5: Mobile Safari trató históricamente las unidades de viewport y las barras dinámicas de forma inconsistente, lo que hacía que “sticky + layouts con 100vh” fuera una forma fiable de perder una tarde.
  • Hecho 6: Las transformaciones CSS (transform) crean nuevos bloques contenedores y contextos de apilamiento; se usaron intensamente para aceleración por GPU mucho antes de que la gente se diera cuenta de que pueden cambiar el comportamiento contenedor de sticky.
  • Hecho 7: Sticky dentro de elementos de tabla (thead, th) ha tenido soporte desigual a lo largo de los años; mejoró, pero el “layout de tabla” todavía tiene comportamientos especiales comparado con el layout de bloque.
  • Hecho 8: La impresión y los medios paginados influyeron en los motores de maquetación temprano; sticky no aplica allí, pero el legado de “fragmentación” y “contención” sigue moldeando lo que los navegadores consideran un modelo de posicionamiento seguro.

Patrones comprobados: cabeceras, barras laterales y tablas

Sticky puede ser aburrido. Quieres aburrido. Lo aburrido se envía.

Patrón A: Cabecera sticky global (desplazamiento del documento)

Úsalo cuando el documento se desplaza (sin scroller anidado). Manténlo simple:

  • Pon la cabecera cerca de la parte superior del DOM.
  • Dale position: sticky y top: 0.
  • Dale un fondo y un z-index con intención.

Detalle clave: No confíes en la transparencia a menos que quieras que el contenido se vea a través al hacer scroll. Si lo haces, añade un sutil backdrop-filter y acepta que tiene implicaciones de soporte en navegadores.

Patrón B: Barra lateral sticky dentro de una sección de la página

Plantilla clásica de documentación: navegación izquierda, contenido principal, “en esta página” a la derecha. La trampa: la barra lateral se pega en relación al contenedor de desplazamiento más cercano, y el bloque contenedor de la barra lateral suele ser el padre flex.

Haz esto:

  • Haz que la página complete desplace el documento siempre que sea posible.
  • Asegura que la cadena de ancestros de la barra lateral no establezca overflow en el eje de desplazamiento.
  • Usa align-self: flex-start si es necesario para que flex no estire de formas que rompen la altura esperada.

Enfoque fiable: envuelve la barra lateral en una columna que defina el límite que quieres, y aplica sticky a un elemento interno:

  • .sidebar-column define límites y reglas de altura.
  • .sidebar-inner es sticky con top ajustado a la altura de la cabecera.

Patrón C: Cabecera sticky dentro de un contenedor desplazable (modales, paneles)

Aquí, sticky es excelente porque se pega al contenedor, no al viewport—exactamente lo que quieres en un modal con su propio scroll.

Haz esto:

  • Haz que el cuerpo del modal sea el contenedor de desplazamiento (overflow: auto).
  • Pon el elemento sticky dentro de ese contenedor de desplazamiento (no por encima).
  • Sé explícito sobre top, fondo y apilamiento.

Patrón D: Cabeceras sticky en tablas

Cuando necesitas cabeceras de tabla sticky dentro de una caja con scroll:

  • Usa un div envoltorio que haga scroll (overflow: auto).
  • Aplica position: sticky; top: 0 a los elementos th.
  • Establece background en los th para que las filas no se muestren a través.

Chequeo de realidad: El renderizado de tablas puede volverse extraño con z-index y colapso de bordes. Si necesitas precisión al píxel, considera separar cabecera y cuerpo en dos tablas sincronizadas—pero solo si estás dispuesto a mantenerlo.

Por qué falla sticky (modos de fallo que sigues encontrando)

1) El contenedor de desplazamiento incorrecto

Si un ancestro tiene overflow: auto o overflow: hidden (o incluso overflow: clip), el elemento sticky puede quedar restringido a ese ancestro. Este es el desajuste número 1 entre la intención (“pegarse al viewport”) y el comportamiento real (“pegarse a este panel”).

Cómo se ve: Sticky funciona solo dentro de una región pequeña; se detiene pronto; o nunca se pega porque el contenedor en realidad no se desplaza.

2) Sin umbral (o un umbral que es efectivamente “ninguno”)

Sticky necesita un umbral: top o bottom. Si no lo estableces, muchos navegadores no harán nada significativo. Si lo pones en un eje que nunca se desplaza, tampoco pasará nada. Aburrido, pero real.

3) Un ancestro crea un bloque contenedor vía transform/filter/perspective

Las transformaciones y ciertos efectos crean nuevos bloques contenedores y contextos de apilamiento. Sticky es sensible a esto porque el navegador tiene que decidir qué significa “relativo a” cuando un padre es efectivamente un nuevo sistema de coordenadas.

Ofensores comunes:

  • transform: translateZ(0) usado como un truco de “aceleración por GPU”
  • filter o backdrop-filter en un contenedor padre
  • perspective en envoltorios de layout
  • contain: paint u otras configuraciones de contención usadas para aislar rendimiento

4) Trampas de Flexbox y min-height

Sticky dentro de layouts flex puede fallar cuando la altura del padre y el comportamiento de overflow crean restricciones que no pretendías. Un clásico es un contenedor flex de altura completa con valores por defecto de min-height que impiden que el contenedor se desplace, así que sticky nunca alcanza su estado “pegado”.

Dos reglas prácticas:

  • Si tienes un layout flex a altura completa, a menudo necesitas min-height: 0 en los hijos flex que deben poder encogerse y desplazar.
  • No pongas sticky en un elemento que está siendo estirado de una manera que hace ambiguo su “posición normal”.

5) Sorpresas de z-index y contextos de apilamiento

Un sticky que “funciona” pero queda oculto bajo contenido es un problema de apilamiento, no de sticky. Los elementos sticky no flotan automáticamente por encima de todo. Si un hermano crea un nuevo contexto de apilamiento con un z-index mayor, tu cabecera sticky parecerá desaparecer.

6) Desplazamientos de layout y contenido dinámico

Sticky se calcula respecto al layout. Si el contenido carga tarde (anuncios, imágenes sin dimensiones, componentes asíncronos), la posición “pegada” puede saltar. Los usuarios lo llaman “glitchy”; tus métricas lo llaman CLS.

7) Elementos sticky anidados y offsets en conflicto

Dos cabeceras sticky en scrollers anidados pueden superponerse. El navegador no está equivocado; tú lo estás. Decide quién se queda con qué píxeles y coordina offsets explícitamente.

Broma 2/2: Cada vez que anidas un contenedor de desplazamiento, un futuro tú pierde una hora y gana una nueva opinión sobre “CSS simple”.

Guía rápida de diagnóstico

Esta es la secuencia de “deja de adivinar”. Ejecútala en orden. Normalmente encontrarás el problema en el tercer paso.

Primero: confirma el contenedor de desplazamiento

  • Identifica qué elemento realmente desplaza: document, un div de app shell, el cuerpo de un modal o un panel.
  • Revisa los valores de overflow de los ancestros a lo largo del camino del elemento sticky.
  • Si el contenedor de desplazamiento no es el que esperas, arréglalo antes de tocar sticky.

Segundo: confirma los prerrequisitos de sticky

  • El elemento sticky tiene position: sticky y un umbral (top normalmente).
  • El elemento sticky está dentro del contenedor de desplazamiento que esperas.
  • El elemento sticky no está restringido por un bloque contenedor demasiado pequeño.

Tercero: revisa los “mata-sticky”

  • Algún ancestro tiene overflow: hidden/clip en el eje sticky.
  • Algún ancestro tiene transform, filter, perspective o contención que cambian sistemas de coordenadas.
  • El dimensionado de flex/grid evita el desplazamiento (busca min-height: 0 o min-width: 0 faltantes).
  • z-index/contextos de apilamiento ocultan el elemento sticky detrás del contenido.

Cuarto: reproduce en un fragmento mínimo del DOM

  • Copia el elemento sticky y sus ancestros en una página HTML mínima.
  • Quita estilos hasta que sticky empiece a funcionar.
  • La última eliminación es la causa raíz, no “CSS está roto”.

Tareas prácticas: comandos, salidas y decisiones

Los bugs de sticky son bugs de frontend, pero depurarlos se beneficia de hábitos de producción: inspeccionar, capturar estado, comparar entornos y guardar recibos. Aquí hay tareas concretas que puedes ejecutar localmente o en CI para evitar “funciona en mi máquina”.

Tarea 1: Verificar que el CSS desplegado realmente contiene position: sticky

cr0x@server:~$ grep -R "position:\s*sticky" -n dist/assets | head
dist/assets/app.3d9c2.css:1842:.header{position:sticky;top:0;z-index:50}
dist/assets/app.3d9c2.css:9912:.toc{position:sticky;top:72px}

Qué significa la salida: Tu bundle construido incluye reglas sticky y los offsets esperados.

Decisión: Si grep no encuentra nada, tu pipeline de build eliminó o reescribió la regla (config de autoprefixer, extracción de CSS o un paso de “critical CSS”). Arregla las entradas del build antes de depurar el layout.

Tarea 2: Comprobar si una “optimización” añadió overflow: hidden de forma amplia

cr0x@server:~$ grep -R "overflow:\s*hidden" -n src styles | head -n 20
src/layout/Shell.css:12:.shell{overflow:hidden;height:100vh}
src/components/Card.css:4:.card{overflow:hidden;border-radius:12px}
styles/utilities.css:88:.clip{overflow:hidden}

Qué significa la salida: Los envoltorios de layout están recortando overflow. Es un sospechoso principal para el fallo de sticky, especialmente si se aplica al shell raíz.

Decisión: Si está en la ruta de scroll principal, elimínalo o muévelo más abajo en el árbol solo a los elementos que necesitan recorte (cards, imágenes).

Tarea 3: Confirmar que no moviste accidentalmente el scroll a un app shell

cr0x@server:~$ grep -R "overflow:\s*auto" -n src/layout | head
src/layout/Shell.css:16:.content{overflow:auto;min-height:0}

Qué significa la salida: La app desplaza dentro de .content, no el documento. Sticky se anclará a ese contenedor.

Decisión: Pon los elementos sticky como hijos de .content (si deben pegarse dentro de él), o rediseña para que el documento se desplace si necesitas sticky a nivel de viewport.

Tarea 4: Detectar hacks de transform que pueden cambiar el comportamiento contenedor

cr0x@server:~$ grep -R "transform:\s*translateZ(0)" -n src styles | head
src/layout/Shell.css:21:.shell{transform:translateZ(0)}

Qué significa la salida: Alguien usó el clásico “empujón GPU”. Puede crear un nuevo bloque contenedor/contexto de apilamiento.

Decisión: Elimínalo a menos que puedas probar que arregla un problema real de rendimiento. Reemplázalo por transformaciones dirigidas a los elementos animados, no a todo el shell.

Tarea 5: Revisar configuraciones de contención que recortan o aíslan el pintado

cr0x@server:~$ grep -R "contain:" -n src styles | head
src/layout/Shell.css:22:.shell{contain:paint}
src/components/Grid.css:7:.grid{contain:layout paint}

Qué significa la salida: Se está usando contención. Es una optimización válida, y también puede cambiar cómo se posicionan y pintan los descendientes.

Decisión: Si sticky está dentro de un subárbol contenido y se comporta mal, elimina la contención del ancestro o reestructura para que sticky esté fuera de esa región aislada.

Tarea 6: Probar que el elemento sticky está siendo cubierto (auditoría de z-index)

cr0x@server:~$ grep -R "z-index" -n src/components src/layout | head -n 25
src/layout/Header.css:9:.header{z-index:10}
src/components/Modal.css:3:.backdrop{z-index:100}
src/components/Popover.css:5:.popover{z-index:200}
src/components/Content.css:44:.content{position:relative;z-index:50}

Qué significa la salida: Tu cabecera sticky tiene z-index: 10, pero contenido u overlays pueden estar por encima.

Decisión: O sube el z-index de la cabecera en el contexto de apilamiento adecuado, o elimina el z-index competidor de elementos que no deberían estar por encima de una cabecera global.

Tarea 7: Identificar scrollers anidados en una página en ejecución usando Playwright (headless)

cr0x@server:~$ node -e "const { chromium } = require('playwright');(async()=>{const b=await chromium.launch();const p=await b.newPage();await p.goto('http://localhost:3000');const scrollers=await p.$$eval('*', els=>els.filter(e=>{const s=getComputedStyle(e);return (s.overflowY==='auto'||s.overflowY==='scroll') && e.scrollHeight>e.clientHeight;}).slice(0,15).map(e=>({tag:e.tagName,id:e.id,cls:e.className,scrollH:e.scrollHeight,clientH:e.clientHeight,overflowY:getComputedStyle(e).overflowY})));console.log(scrollers);await b.close();})();"
[
  { tag: 'DIV', id: 'app', cls: 'shell', scrollH: 3120, clientH: 900, overflowY: 'auto' },
  { tag: 'DIV', id: '', cls: 'modal-body', scrollH: 1410, clientH: 520, overflowY: 'auto' }
]

Qué significa la salida: El app shell y un cuerpo de modal son contenedores de desplazamiento. Sticky dentro de cada uno se pegará respecto a ellos.

Decisión: Decide qué scroller posee sticky. Si quieres sticky a nivel de viewport, deja de desplazar #app y permite que el documento se desplace.

Tarea 8: Capturar estilos calculados para un elemento sticky durante un informe de error

cr0x@server:~$ node -e "const { chromium } = require('playwright');(async()=>{const b=await chromium.launch();const p=await b.newPage();await p.goto('http://localhost:3000/docs');const sel='.toc';const s=await p.$eval(sel, el=>{const cs=getComputedStyle(el);return {position:cs.position,top:cs.top,overflow:cs.overflow,transform:cs.transform,zIndex:cs.zIndex};});console.log(s);await b.close();})();"
{ position: 'sticky', top: '72px', overflow: 'visible', transform: 'none', zIndex: 'auto' }

Qué significa la salida: El elemento es sticky con un offset top válido; no tiene transform en sí mismo.

Decisión: Si esto parece correcto, el problema está casi seguro en un ancestro (overflow/transform/contain) o en el contexto de apilamiento (z-index “auto” más un hermano con contexto de apilamiento).

Tarea 9: Detectar overflow/transform en ancestros para un selector dado (automatizado)

cr0x@server:~$ node -e "const { chromium } = require('playwright');(async()=>{const b=await chromium.launch();const p=await b.newPage();await p.goto('http://localhost:3000/docs');const chain=await p.$eval('.toc', el=>{const out=[];let n=el.parentElement;let i=0;while(n&&i<12){const cs=getComputedStyle(n);out.push({tag:n.tagName,id:n.id,cls:n.className,overflowY:cs.overflowY,transform:cs.transform,contain:cs.contain});n=n.parentElement;i++;}return out;});console.log(chain);await b.close();})();"
[
  { tag: 'ASIDE', id: '', cls: 'toc-column', overflowY: 'visible', transform: 'none', contain: 'none' },
  { tag: 'DIV', id: '', cls: 'content', overflowY: 'auto', transform: 'none', contain: 'none' },
  { tag: 'DIV', id: 'app', cls: 'shell', overflowY: 'hidden', transform: 'translateZ(0)', contain: 'paint' }
]

Qué significa la salida: Tu elemento sticky está dentro de .content (contenedor de desplazamiento), mientras que #app recorta overflow y aplica transform/containment. Eso es un campo minado para sticky.

Decisión: Elimina overflow:hidden de #app, quita el hack de transform, o mueve sticky fuera de ese subárbol.

Tarea 10: Confirmar que el elemento realmente alcanza el umbral de pegado (prueba de desplazamiento)

cr0x@server:~$ node -e "const { chromium } = require('playwright');(async()=>{const b=await chromium.launch();const p=await b.newPage();await p.goto('http://localhost:3000/docs');await p.evaluate(()=>{const sc=document.querySelector('.content');sc.scrollTop=0;});const before=await p.$eval('.toc', el=>el.getBoundingClientRect().top);await p.evaluate(()=>{const sc=document.querySelector('.content');sc.scrollTop=400;});const after=await p.$eval('.toc', el=>el.getBoundingClientRect().top);console.log({before,after});await b.close();})();"
{ before: 184, after: 72 }

Qué significa la salida: El top del elemento pasó a 72px tras el scroll: sticky se activó correctamente dentro de ese scroller.

Decisión: Si after sigue cambiando (no se estabiliza), sticky no se está activando—busca restricciones de overflow/transform o falta de top.

Tarea 11: Capturar cambios de layout que hacen que sticky parezca temblar (proxy CLS vía diffs de capturas)

cr0x@server:~$ node -e "const { chromium } = require('playwright');(async()=>{const b=await chromium.launch();const p=await b.newPage({viewport:{width:1280,height:720}});await p.goto('http://localhost:3000/docs');await p.waitForTimeout(200);await p.screenshot({path:'s1.png',fullPage:false});await p.waitForTimeout(2000);await p.screenshot({path:'s2.png',fullPage:false});console.log('captured s1.png and s2.png');await b.close();})();"
captured s1.png and s2.png

Qué significa la salida: Se tomaron dos capturas tempranas y después de que probablemente cargó contenido asíncrono.

Decisión: Si las posiciones de la cabecera/TOC difieren entre imágenes, tienes contenido que carga tarde o swaps de fuentes que cambian el layout. Soluciona reservando espacio (dimensiones explícitas de imágenes, estrategia font-display, skeletons).

Tarea 12: Verificar reglas de overflow y altura en el CSS construido para tu shell de layout

cr0x@server:~$ sed -n '1,120p' src/layout/Shell.css
.shell{
  height:100vh;
  overflow:hidden;
  transform:translateZ(0);
  contain:paint;
}
.content{
  display:flex;
  overflow:auto;
  min-height:0;
}

Qué significa la salida: El shell es una caja recortada, transformada y contenida para pintura. El contenido se desplaza dentro de ella.

Decisión: Si quieres una cabecera sticky relativa al viewport, esta arquitectura te está peleando. O permites que el documento se desplace, o aceptas sticky por contenedor y diseñas offsets en consecuencia.

Tarea 13: Comprobar si una regresión coincide con un cambio reciente (git blame con intención)

cr0x@server:~$ git blame -L 1,40 src/layout/Shell.css
a81c9d12 (devA 2025-10-03 10:14:02 +0000  1) .shell{
a81c9d12 (devA 2025-10-03 10:14:02 +0000  2)   height:100vh;
a81c9d12 (devA 2025-10-03 10:14:02 +0000  3)   overflow:hidden;
a81c9d12 (devA 2025-10-03 10:14:02 +0000  4)   transform:translateZ(0);
a81c9d12 (devA 2025-10-03 10:14:02 +0000  5)   contain:paint;
a81c9d12 (devA 2025-10-03 10:14:02 +0000  6) }

Qué significa la salida: Un único cambio introdujo múltiples “mata-sticky” a la vez.

Decisión: Revierte o divide el cambio. Si la motivación fue rendimiento, mide adecuadamente y reintroduce solo lo que puedas justificar.

Tarea 14: Confirmar que el elemento sticky no está accidentalmente dentro de un ancestro que recorta (volcado DOM)

cr0x@server:~$ node -e "const { JSDOM } = require('jsdom');const fs=require('fs');const html=fs.readFileSync('dist/index.html','utf8');const dom=new JSDOM(html);const el=dom.window.document.querySelector('.header');let n=el;let i=0;while(n&&i<8){console.log(i,n.tagName,n.id,n.className);n=n.parentElement;i++;}"
0 HEADER  header
1 DIV app shell
2 BODY  
3 HTML  

Qué significa la salida: La cabecera es hija del shell transformado/recortado.

Decisión: Si el shell no está pensado para ser una capa de contención/recorte, reestructura: coloca la cabecera global como un hermano del contenedor desplazable, o elimina el recorte a nivel de shell.

¿Notas un patrón? No estamos “probando CSS al azar”. Estamos demostrando qué se desplaza, qué contiene, qué recorta y qué se apila. Sticky funciona cuando tratas el DOM como un sistema.

Tres mini-historias corporativas desde las trincheras del sticky

Mini-historia 1: El incidente causado por una suposición errónea

La compañía tenía una consola de soporte al cliente con una barra sticky de “estado del caso” en la parte superior del panel principal. Funcionó bien durante meses. Luego llegó un rediseño: la consola se movió a un nuevo app shell con una navegación izquierda persistente y una “mejora de rendimiento” que evitó que el documento se desplazara. El scroll pasó a .content.

Los agentes de soporte empezaron a reportar que la barra de estado a veces desaparecía al desplazarse en casos largos. Algunos pensaron que era un bug de permisos porque ocurría más en casos complejos (que eran más largos y requerían más scroll). La triage etiquetó el bug como “renderizado intermitente”. Esa etiqueta debería ser ilegal.

La suposición errónea: “sticky se pega al viewport”. No lo hacía. Se pegaba al contenedor de desplazamiento más cercano, que ahora era un div anidado. Pero la barra de estado ya no estaba dentro de ese div—era un hermano. Así que nunca se activaba. En algunas pantallas parecía “bien” porque la barra coincidía con la visibilidad debido a la altura del layout y el viewport.

La solución no fue exótica: o mueves la barra dentro del contenedor desplazable y la haces sticky allí, o permites que el documento se desplace y la mantienes sticky respecto al viewport. Eligieron lo primero. El incidente terminó, y el equipo actualizó sus guías de layout: quien crea el contenedor de desplazamiento es el dueño del comportamiento sticky. No negociable.

Mini-historia 2: La optimización que salió mal

Un equipo distinto construyó un sitio de base de conocimiento con un TOC sticky “En esta página”. Era limpio, rápido y estable—hasta que alguien ejecutó un proyecto de “reducir coste de paint”. Esparcieron contain: paint en regiones de layout y añadieron transform: translateZ(0) al contenedor raíz para “promoverlo a su propia capa”.

Midieron una mejora en un micro-benchmark: una prueba sintética de scroll en un dispositivo Android de gama media. Luego lo desplegaron. Una semana después, reportes de bugs: el TOC dejó de pegarse en Safari y la cabecera ocasionalmente se renderizaba debajo del contenido al desplazarse. Parecía un problema de z-index pero no se solucionaba de forma consistente con cambios de z-index.

La causa raíz fue un cóctel: el root transformado creó un nuevo bloque contenedor y contexto de apilamiento; la contención de paint cambió cómo se recortaban y pintaban los descendientes; y el elemento sticky estaba ahora dentro de un subárbol que el navegador trataba de forma distinta durante la composición del scroll. Diferentes motores manejaron la combinación de forma distinta.

Revertieron la “optimización”, luego la reintrodujeron selectivamente: solo en componentes que realmente animaban, no en el root. El rendimiento se mantuvo bien, sticky volvió a ser aburrido, y el equipo aprendió la lección que operaciones sigue enseñando: optimizar sin un arnés de corrección es simplemente romper cosas eficientemente.

Mini-historia 3: La práctica aburrida pero correcta que salvó el día

Un dashboard fintech tenía múltiples capas sticky: una cabecera global, una subbarra de navegación y una cabecera por tabla sticky dentro de una rejilla desplazable. Es el tipo de UI que se ve genial en un mock y se convierte en un vivero de bugs en producción.

En lugar de “probar y ver”, el equipo escribió un pequeño conjunto de pruebas automatizadas de layout usando Playwright. No snapshots visuales sofisticados—solo aserciones geométricas: identificación del contenedor de desplazamiento, comprobaciones de boundingClientRect después del scroll, y una comprobación de que el top de la cabecera sticky equals el offset esperado.

Meses después, un refactor cambió un wrapper a overflow: hidden para arreglar un tema de esquinas redondeadas. Las pruebas de sticky fallaron en CI inmediatamente. El desarrollador vio la falla, movió el recorte a un elemento hijo y preservó el comportamiento de overflow del contenedor desplazable. Ningún cliente lo notó. Nadie escribió un hilo largo en Slack sobre Safari.

Esta es la verdad poco sexy: la fiabilidad de sticky viene de las barreras. Tu yo futuro no recordará por qué min-height: 0 importaba en ese hijo flex. Las pruebas lo recordarán.

Errores comunes: síntoma → causa raíz → solución

Sticky nunca se activa (se desplaza como algo normal)

Síntoma: El elemento se comporta como position: relative; nunca se fija en la parte superior.

Causa raíz: Falta top/bottom, o el elemento sticky no está en la caja de desplazamiento que crees.

Solución: Añade top: 0 (o el offset correcto). Confirma el scroller con una auditoría de la cadena de ancestros. Mueve sticky dentro del contenedor de desplazamiento.

Sticky funciona, pero se detiene demasiado pronto

Síntoma: El elemento se pega brevemente, luego se desplaza fuera antes de que termine la sección.

Causa raíz: El bloque contenedor (a menudo el padre) es más corto que el contenido desplazable, así que sticky queda limitado y alcanza el final del contenedor.

Solución: Haz que el elemento límite previsto envuelva toda la región donde sticky debe operar. Evita aplicar sticky a un elemento cuyo padre tenga altura limitada inesperadamente.

Sticky funciona, pero queda oculto bajo contenido

Síntoma: Está “ahí”, pero el contenido se desplaza por encima.

Causa raíz: Problemas de contexto de apilamiento y z-index (a menudo causados por transforms o elementos posicionados con z-index).

Solución: Dale al elemento sticky un z-index no auto dentro del contexto de apilamiento correcto. Elimina z-index innecesarios de hermanos. Evita transforms a nivel raíz que creen contextos de apilamiento.

Sticky parpadea o tiembla durante el scroll

Síntoma: Al desplazarse, el elemento sticky se sacude, repinta o salta un píxel.

Causa raíz: Redondeo subpíxel + cambios de composición + cambios dinámicos de contenido; a veces empeorado por la carga de fuentes o backdrop-filter.

Solución: Reduce la complejidad de composición: evita combinar sticky con efectos pesados en ancestros. Reserva espacio para fuentes/imagenes para evitar shifts tardíos en el layout.

Sticky falla solo en un modal

Síntoma: Funciona en la página, falla dentro de diálogos modales.

Causa raíz: El cuerpo del modal es el scroller; el elemento sticky está fuera de él, o un ancestro recorta overflow.

Solución: Coloca las cabeceras sticky dentro del cuerpo desplazable del modal. Haz explícito que el cuerpo del modal es el contenedor de desplazamiento.

La barra lateral sticky se superpone al pie u otra sección

Síntoma: La barra lateral sigue pegada y cubre contenido cerca del final.

Causa raíz: El límite sticky es demasiado grande (p. ej., el contenedor de la barra lateral abarca toda la página), o los offsets no consideran el contenido inferior.

Solución: Restringe el bloque contenedor de la barra lateral a la sección donde debe pegarse. Considera cambiar a position: sticky; bottom: ... para ciertos patrones, pero hazlo deliberadamente.

Sticky falla solo en Safari / iOS

Síntoma: Funciona en Chromium/Firefox pero no en Safari.

Causa raíz: Una combinación de desplazamiento anidado, transforms y efectos; o el comportamiento de unidades de viewport que causa diferencias de layout que cambian umbrales.

Solución: Elimina transforms/contención de ancestros, simplifica contenedores de desplazamiento y valida con comprobaciones geométricas automatizadas en WebKit. Prefiere container-sticky dentro de un único scroller con overflow cuando construyas shells complejos.

Sticky dentro de una columna flex no se comporta como esperas

Síntoma: Se pega, pero no en el momento correcto; o no se pega cuando el contenido es corto.

Causa raíz: El dimensionado de flex y valores por defecto de min-size impiden el scroll o cambian la altura del bloque contenedor.

Solución: Añade min-height: 0 a los hijos flex que necesitan desplazar; asegura que el contenedor del elemento sticky tenga la altura y configuraciones de overflow correctas.

Listas de verificación / plan paso a paso

Checklist: cabecera sticky que debe pegarse al viewport

  1. Haz que el documento se desplace (evita desplazar dentro de #app a menos que sea necesario).
  2. Asegura que ningún ancestro de la cabecera sticky establezca overflow en el eje vertical (hidden, clip, auto), a menos que pretendas sticky por contenedor.
  3. Evita transform, filter, contain a nivel raíz en envoltorios que contienen la cabecera.
  4. Establece position: sticky; top: 0 en la cabecera.
  5. Define un fondo y un z-index con intención.
  6. Verifica el comportamiento con un scroll automatizado + aserción de boundingClientRect.

Checklist: barra lateral sticky que debe detenerse al final de la sección

  1. Crea un envoltorio que defina el límite de la barra lateral (la columna de la sección).
  2. Pon un elemento interno sticky dentro de ese envoltorio.
  3. Fija top para despejar la cabecera global (no lo adivines; usa un token o variable CSS).
  4. Asegura que el envoltorio no tenga overflow que recorte en el eje sticky.
  5. Si usas flex/grid, verifica que la altura del envoltorio límite corresponda a la altura de la sección que esperas.
  6. Prueba con contenido largo y corto, y con contenido que carga tarde.

Checklist: sticky dentro de un modal o panel desplazable

  1. Haz que un único elemento sea el contenedor de desplazamiento: overflow: auto en el cuerpo del modal.
  2. Coloca cabeceras sticky dentro de ese contenedor de desplazamiento.
  3. Asegura que el umbral sticky tenga en cuenta cualquier chrome fijo del modal.
  4. Mantén mínimos los transforms/efectos en ancestros; aplica efectos a hermanos en lugar de padres si es posible.
  5. Prueba en WebKit si envías a iOS.

Paso a paso: cuando sticky está roto y necesitas arreglarlo hoy

  1. Encuentra el contenedor de desplazamiento usando un escaneo automatizado (como la tarea de detección de scrollers de Playwright). Decide si eso es correcto.
  2. Imprime la cadena de ancestros para el elemento sticky y busca overflow, transform, contain, filter.
  3. Quita temporalmente propiedades sospechosas (en devtools) empezando por el ancestro más cercano hacia afuera.
  4. Arregla la arquitectura: deja de anidar scrollers, o acepta container-sticky y mueve el nodo sticky dentro del scroller.
  5. Afírmalo con una prueba geométrica para que el mismo bug no vuelva en el próximo sprint disfrazado.

Preguntas frecuentes

1) ¿Por qué deja de funcionar position: sticky cuando añado overflow: hidden a un padre?

Porque el recorte por overflow cambia el contexto de desplazamiento/bloque contenedor del que depende sticky. En muchos casos lo restringe al cuadro de ese ancestro o impide la relación de desplazamiento necesaria. Mueve el recorte de overflow a un elemento de nivel inferior (como la tarjeta visual) en vez del envoltorio de layout.

2) ¿Sticky es relativo al viewport o a la página?

Ninguno, por defecto. Sticky es relativo al contenedor de desplazamiento relevante más cercano para ese eje. Si el documento se desplaza y ningún ancestro crea un contexto de scroll/recorte, parecerá relativo al viewport.

3) ¿Cuándo debo usar position: fixed en lugar de sticky?

Usa fixed cuando el elemento debe permanecer anclado al viewport sin importar dónde esté en el DOM, y estás dispuesto a manejar offsets de layout (padding/márgenes) manualmente. Usa sticky cuando el elemento solo deba pegarse dentro de una sección o límite de contenedor.

4) ¿Por qué mi cabecera sticky superpone contenido?

Sticky no reserva espacio como lo hace una cabecera estática; cambia de posicionamiento durante el scroll. Da al contenido un padding/margin-top igual a la altura de la cabecera si la cabecera está superponiéndose. También asegura que la cabecera tenga fondo y apilamiento correcto para coherencia visual.

5) ¿Puedo tener dos cabeceras sticky (global + local)?

Sí, pero debes coordinar offsets. El top del elemento sticky local debe tener en cuenta la altura del sticky global. Si están en diferentes contenedores de desplazamiento, reconsidera el layout—sticky anidado a través de scrollers anidados es donde nacen los bugs.

6) ¿Por qué se comporta diferente sticky en Safari?

Safari es más sensible a combinaciones de desplazamiento anidado, transforms y efectos que crean nuevos bloques contenedores o capas de composición. La solución práctica es arquitectónica: reduce scrollers anidados y evita transforms/contención en ancestros de elementos sticky.

7) ¿Funciona z-index en elementos sticky?

Sí, pero solo dentro del contexto de apilamiento relevante. Si un ancestro crea un nuevo contexto de apilamiento (a menudo vía transform o un elemento posicionado con z-index), puedes aumentar z-index todo lo que quieras y aún así perder frente a un hermano en otro contexto. Arregla primero contextos de apilamiento, luego z-index.

8) ¿Por qué sticky “no se pega” dentro de un layout flex?

A menudo porque el elemento que debería desplazar no está realmente desplazando: los items flex tienen valores por defecto de min-size que pueden prevenir que se encojan, así que el contenedor no obtiene scrollbar. Añade min-height: 0 al hijo flex correcto y verifica las configuraciones de overflow.

9) ¿Se justifica alguna vez un sticky basado en JavaScript (escuchas de scroll)?

A veces—cuando necesitas comportamiento que CSS no puede expresar (detección compleja de colisiones, snapping, límites dinámicos). Pero trátalo como una característica de rendimiento con presupuesto: usa IntersectionObserver cuando sea posible, evita lecturas/escrituras de layout por frame, y prueba en dispositivos de gama baja.

10) ¿Cómo prevengo el jitter de sticky causado por contenido dinámico?

Reserva espacio: dimensiones explícitas para imágenes, estrategia de carga de fuentes estable, y evita inyectar DOM encima de regiones sticky tras el render inicial. Si el contenido debe cambiar, considera animar cambios de altura cuidadosamente y verifica el top computado del elemento sticky durante pruebas de scroll.

Próximos pasos que realmente puedes hacer

Sticky funciona cuando dejas de tratarlo como magia y empiezas a tratarlo como un sistema con restricciones: contenedores de desplazamiento, bloques contenedores y contextos de apilamiento. Decide dónde vive el desplazamiento, coloca sticky dentro de ese contexto de scroll y elimina estilos “útiles” de envoltorio que cambian silenciosamente los sistemas de coordenadas.

Haz esto a continuación, en orden:

  1. Elige un scroller primario para cada experiencia (página, modal, panel). Evita anidar a menos que tengas una razón potente.
  2. Ejecuta una auditoría de cadena de ancestros en cada elemento sticky (overflow, transform, contain, filter, z-index).
  3. Estandariza offsets sticky usando variables CSS (la altura de la cabecera no debe ser una suposición).
  4. Añade una prueba geométrica automatizada por componente sticky: desplaza, mide boundingClientRect, aserta el umbral sticky.

Si quieres sticky sin dolor, constrúyelo como construyes sistemas en producción: haz el entorno predecible, mide lo que el navegador está haciendo y quita las trampas antes de que te quiten el fin de semana.

← Anterior
Spectre/Meltdown: cuando las CPUs se convirtieron en la noticia de seguridad del año
Siguiente →
Construye una tabla de precios SaaS que convierta sin romper tu frontend

Deja un comentario