wandres.dev
SCROLL · Snap, overscroll y barras

overscroll-behavior y el encadenamiento del scroll

Qué es el scroll chaining, cómo se corta con contain, qué añade none, y la limitación de implementación que hace que la propiedad no siempre haga efecto.

⏱ 16 min

Llegas al final de un panel con scroll y sigues deslizando: el navegador empieza a desplazar la página de detrás. Ese comportamiento se llama encadenamiento del scroll, tiene sentido en un documento y es un desastre en un panel lateral, un modal o un mapa. overscroll-behavior lo controla con una declaración, y de paso permite desactivar el rebote elástico y el gesto de recargar tirando hacia abajo.

🎯 Al terminar esta lección sabrás
  • Explicar qué es el encadenamiento del scroll y cuándo molesta.
  • Elegir entre contain y none según el efecto que quieras cortar.
  • Aplicarlo por ejes con las variantes lógicas.
  • Conocer la limitación de implementación que hace que a veces no surta efecto.

Los tres valores

overscroll-behavior se declara en el contenedor de scroll y controla qué ocurre cuando el usuario intenta desplazarse más allá de su límite.

auto. El valor por defecto. El desbordamiento del gesto se propaga al ancestro con scroll: eso es el encadenamiento. Además se permiten los efectos propios de la plataforma: el rebote elástico de macOS e iOS, y el gesto de recargar tirando hacia abajo en móvil.

contain. Corta el encadenamiento: el gesto no pasa al ancestro. Pero mantiene los efectos locales de la plataforma, así que el rebote elástico dentro del propio contenedor sigue funcionando.

none. Corta el encadenamiento y desactiva los efectos locales. Ni rebote, ni recarga por gesto.

En la práctica, contain es el valor que quieres el noventa por ciento de las veces. none solo cuando el rebote interfiere con lo que hace el componente: un lienzo que responde al gesto, un mapa, una superficie de dibujo.

.panel-lateral { overflow-y: auto; overscroll-behavior: contain; }
.mapa          { overflow: hidden; overscroll-behavior: none; }

Existe una versión por ejes, overscroll-behavior-x y overscroll-behavior-y, y sus variantes lógicas overscroll-behavior-block y overscroll-behavior-inline. El atajo acepta uno o dos valores.

/* un carrusel horizontal: no arrastra la página al llegar al final */
.carrusel { overscroll-behavior-inline: contain; overscroll-behavior-block: auto; }

Ese caso concreto es de los que más se agradecen: sin la declaración, deslizar horizontalmente hasta el final de un carrusel en un móvil hace que la página empiece a moverse en horizontal, o dispare el gesto de navegación atrás del sistema.

Los tres casos donde importa

El modal con scroll. Un diálogo largo tiene su propio scroll. Al llegar al final, sin contain, el gesto continúa y desplaza la página de detrás, con lo que al cerrar el modal el usuario está en otro sitio del documento.

dialog { max-block-size: 80dvh; overflow-y: auto; overscroll-behavior: contain; }

Conviene saber que esto sustituye a la técnica clásica de bloquear el scroll del cuerpo poniendo overflow: hidden en body al abrir el modal. Esa técnica tenía dos problemas conocidos: en iOS no funcionaba de forma fiable, y al restaurar el overflow la página perdía la posición de scroll salvo que la guardaras y la restauraras a mano. overscroll-behavior: contain en el propio diálogo no toca el body y no tiene ninguno de los dos.

El panel de chat o de notificaciones. Mismo caso: una lista con scroll propio dentro de una página con scroll.

El componente que gestiona su propio gesto. Un mapa, un editor de imágenes, un lienzo. Aquí none es lo correcto, porque el rebote elástico interfiere con la interpretación del gesto.

flowchart TB
A[Un contenedor con scroll propio llega a su limite] --> B{Que valor tiene overscroll-behavior}
B -->|auto| C[El gesto pasa al ancestro y la pagina se mueve]
C --> D[Ademas hay rebote elastico y recarga por gesto]
B -->|contain| E[El gesto no pasa al ancestro]
E --> F[El rebote elastico local se mantiene]
B -->|none| G[El gesto no pasa y no hay rebote ni recarga]
style A fill:#89b4fa,color:#11111b
style C fill:#f38ba8,color:#11111b
style E fill:#a6e3a1,color:#11111b
style F fill:#a6e3a1,color:#11111b
style G fill:#f9e2af,color:#11111b

La limitación que hay que conocer

Aquí está el detalle que evita un diagnóstico erróneo. Durante años, todas las implementaciones compartieron la misma limitación: la propiedad no tiene efecto en contenedores de scroll que no tienen desbordamiento.

Es decir, si declaras overscroll-behavior: contain en un panel cuyo contenido cabe entero —no hay nada que desplazar—, el gesto sobre ese panel sigue propagándose al ancestro. Y eso es justo lo contrario de lo que quieres en un modal cuyo contenido a veces cabe y a veces no: el comportamiento cambia según la longitud del contenido, que es la clase de inconsistencia más difícil de reproducir en un informe de error.

El estado a agosto de 2026:

  • Chrome lo corrigió en la versión 144.
  • Firefox lo corrigió en la versión 150.
  • Safari todavía tiene la limitación desde que implementó la propiedad en la 16.

Si tu componente puede quedarse sin desbordamiento y necesitas que el gesto no se propague en todos los motores, la vía complementaria es asegurar que siempre hay algo que desplazar, aunque sea un píxel, o interceptar el gesto. Pero para el caso normal —un modal largo, un panel de chat— la propiedad hace exactamente lo que promete.

Hay además un cuarto valor en camino, chain, que permite encadenar explícitamente saltándose ancestros intermedios. A agosto de 2026 solo está en Chrome 150 y sigue marcado como experimental.

El encadenamiento es correcto por defecto, y por eso hay que apagarlo y no al revés

Es fácil ver el encadenamiento como un fallo de diseño de la plataforma y preguntarse por qué no está desactivado por defecto. La respuesta explica bastante sobre cómo se toman estas decisiones. Piensa en el caso mayoritario de un contenedor con scroll en la web: no es un modal, es un fragmento de contenido dentro de un documento —un bloque de código ancho, una tabla, una cita larga—. Si el encadenamiento estuviera desactivado por defecto, el usuario que empieza a deslizar sobre uno de esos bloques se quedaría atrapado: la página no se movería, y en un móvil, donde el dedo cae donde cae, eso ocurriría constantemente. El comportamiento por defecto está elegido para que el gesto de “mover la página” funcione siempre, aunque el dedo aterrice sobre un elemento con scroll propio, que es lo que el usuario quiere en la inmensa mayoría de los casos. Los casos donde molesta —el modal, el panel superpuesto, el mapa— tienen todos algo en común: son superficies que el usuario percibe como separadas del documento, no como parte de él. Y esa es la regla operativa que se deduce y que vale más que memorizar los tres valores: declara contain exactamente en los elementos que el usuario percibe como una superficie aparte, y déjalo en auto en todo lo que sea contenido dentro del flujo del documento. Si dudas de a qué categoría pertenece un elemento, mírate a ti mismo usándolo: si esperabas que la página se moviera, es contenido.

⚔️ Corta la cadena
  1. Monta un panel con scroll dentro de una página larga y comprueba el encadenamiento al llegar al final.
  2. Añade overscroll-behavior: contain y verifica que se corta.
  3. Comprueba en macOS o iOS que el rebote elástico local sigue funcionando con contain y desaparece con none.
  4. Reduce el contenido del panel hasta que quepa entero y observa si la propiedad sigue surtiendo efecto en tu navegador.
  5. Sustituye la técnica de bloquear el scroll con overflow: hidden en body por contain en el diálogo y compara el comportamiento al cerrar.