overflow: hidden, el asesino silencioso del sticky
Por qué cualquier overflow distinto de visible en un ancestro rompe el pegado, la regla de coerción entre ejes que casi nadie conoce, y por qué overflow: clip es la corrección exacta.
De los tres fallos de sticky, este es el que más tiempo cuesta encontrar, porque la propiedad culpable puede estar a diez niveles de distancia, puesta hace dos años por otra persona, para resolver un problema que ya no existe. Y la explicación completa incluye una regla de la especificación que casi nadie conoce: escribir overflow-x: hidden cambia también el valor computado de overflow-y.
- Explicar por qué un ancestro con
overflowsecuestra el pegado de un sticky. - Enunciar la regla de coerción entre
overflow-xyoverflow-y. - Sustituir
overflow: hiddenporoverflow: clipcuando corresponda. - Localizar el ancestro culpable con una sola línea de consola.
Qué hace exactamente un overflow
Un elemento con overflow distinto de visible y de clip se convierte en un contenedor de scroll: define un scrollport, tiene una posición de scroll, y responde a los eventos de desplazamiento aunque su barra esté oculta o no haya nada que desbordar.
Un sticky se pega respecto a su contenedor de scroll más cercano. En el momento en que un ancestro se convierte en contenedor de scroll, deja de ser el viewport quien manda: manda ese ancestro. Y si ese ancestro no desborda —porque su contenido cabe entero— nunca hace scroll, su posición de scroll es siempre cero, el umbral nunca se cruza y el sticky nunca se mueve.
Esto es coherente y, visto desde la especificación, es lo único que podía ocurrir. Lo desconcertante no es la regla, es que la propiedad se aplique en un sitio y el efecto aparezca en otro.
Los valores que crean contenedor de scroll:
| valor | ¿crea contenedor de scroll? | ¿recorta? |
|---|---|---|
visible |
no | no |
hidden |
sí | sí |
scroll |
sí | sí |
auto |
sí | sí |
clip |
no | sí |
La última fila es la clave de esta lección. overflow: clip recorta igual que hidden pero no crea contenedor de scroll: no tiene posición de scroll, no se puede desplazar ni con JavaScript, y no secuestra a los sticky de dentro. Está disponible en los cuatro motores desde Safari 16, en septiembre de 2022.
La regla de coerción entre ejes
Aquí está la parte que explica el caso más común y más invisible. overflow es un atajo de overflow-x y overflow-y, y la especificación define una coerción entre los dos:
Si uno de los dos ejes tiene un valor computado que no es
visibleniclip, y el otro tienevisibleoclip, ese otro se computa aauto.
Traducido: no existe un elemento que recorte en un eje y desborde libremente en el otro si el recorte es de los que crean scroll. La razón es geométrica: un contenido que se sale por la derecha y es recortado, pero que puede salirse libremente por abajo, produciría un rectángulo de scroll indefinido.
La consecuencia práctica es demoledora para los sticky:
/* intención: evitar el scroll horizontal */
body { overflow-x: hidden; }
/* efecto real: overflow-y se computa a auto */
/* body pasa a ser contenedor de scroll y TODOS los sticky de la página lo toman como referencia */
Este patrón está en incontables bases de código, puesto para tapar un elemento que se sale de ancho. Y rompe todos los sticky de la página de golpe, incluidos los de componentes que funcionaban perfectamente en aislamiento.
La corrección exacta es cambiar el valor, no quitarlo:
body { overflow-x: clip; }
Como clip está en el conjunto visible o clip, la coerción no se dispara: overflow-y se queda en visible, body no se convierte en contenedor de scroll, y los sticky vuelven a referirse al viewport. El recorte horizontal sigue funcionando.
Conviene añadir la corrección de fondo: si algo se sale de ancho, lo correcto es arreglar el elemento que se sale. overflow-x: clip es un buen cinturón de seguridad, no un sustituto del diagnóstico.
flowchart TB
A[Escribes overflow-x hidden] --> B[La especificacion coerciona el otro eje]
B --> C[overflow-y computa a auto]
C --> D[El elemento se convierte en contenedor de scroll]
D --> E[Todos los sticky de dentro se refieren a el]
E --> F{Ese contenedor desborda}
F -->|No| G[Nunca hace scroll y el sticky no se pega]
F -->|Si| H[El sticky se pega respecto a el, no al viewport]
A --> I[Escribes overflow-x clip en su lugar]
I --> J[No hay coercion y overflow-y sigue visible]
J --> K[No hay contenedor de scroll y el sticky funciona]
style A fill:#f9e2af,color:#11111b
style G fill:#f38ba8,color:#11111b
style H fill:#f9e2af,color:#11111b
style I fill:#89b4fa,color:#11111b
style K fill:#a6e3a1,color:#11111bEl diagnóstico en una línea
Cuando un sticky no se pegue y sospeches de este caso, esta línea en la consola te da la lista de ancestros culpables sin abrir el inspector.
(function (sel) {
let n = document.querySelector(sel);
const malos = [];
while (n && (n = n.parentElement)) {
const s = getComputedStyle(n);
if (s.overflowX !== 'visible' || s.overflowY !== 'visible') {
malos.push({ nodo: n, x: s.overflowX, y: s.overflowY });
}
}
console.table(malos);
})('.mi-sticky');
Lee overflowX y overflowY computados, no el atajo, precisamente porque la coerción se ve ahí y no en lo que escribiste. Un body con overflow-x: hidden aparecerá en la tabla con y: "auto", y esa columna es la prueba.
Los tres culpables habituales, por frecuencia:
overflow-x: hidden en html o body. Ya visto. Corrección: clip.
overflow: hidden para recortar esquinas redondeadas. Un contenedor con border-radius y una imagen dentro. Corrección: overflow: clip, que recorta igual.
overflow: auto en un panel que a veces desborda y a veces no. Aquí el sticky funciona de forma intermitente según el contenido, que es el síntoma más confuso de todos. Corrección: si el panel debe hacer scroll, el sticky de dentro debe pegarse respecto a él, y hay que ajustar los umbrales a la geometría del panel, no a la de la ventana.
La historia de overflow: clip explica por qué el bug existe y por qué la corrección es tan limpia. overflow: hidden se especificó en CSS 2 con una definición que juntaba dos cosas independientes: recortar lo que se sale y poder desplazarse programáticamente. En 1998 esa unión parecía natural, porque si recortas contenido lo lógico es dejar una forma de llegar a él, aunque sea con JavaScript. Pero convertir un elemento en contenedor de scroll tiene una cascada de consecuencias que nadie pidió: establece un contexto de formato de bloque, contiene los flotantes, impide el colapso de márgenes hacia fuera, es el ancla de los sticky de dentro, participa en el encadenamiento de scroll, y desde 2019 también decide contra qué se resuelven scroll-padding y los umbrales de scroll-snap. Cada una de esas consecuencias es razonable por separado; juntas convierten overflow: hidden en una de las declaraciones con más efectos secundarios de todo el lenguaje. overflow: clip es la reparación: separa las dos ideas y te da el recorte puro, sin scrollport y sin ninguna de las consecuencias anteriores. La conclusión operativa que merece la pena adoptar: si escribes hidden y no esperas que ese elemento haga scroll nunca, lo que querías era clip. En 2026 ya no hay razón de compatibilidad para no hacerlo.
- Pon
overflow-x: hiddenenbodyy comprueba en DevTools queoverflow-ycomputa aauto. - Verifica que con ese cambio todos los sticky de la página dejan de pegarse.
- Cámbialo a
overflow-x: clipy confirma que vuelven a funcionar y que el recorte horizontal se mantiene. - Ejecuta el script de diagnóstico sobre un sticky roto y localiza el ancestro culpable.
- Sustituye un
overflow: hiddenpuesto para redondear esquinas poroverflow: clipy comprueba que el recorte es idéntico.