Take control of your scroll: customizing pull-to-refresh and overflow effects

TL;DR

The CSS overscroll-behavior property allows developers to override the
browser's default overflow scroll behavior when reaching the top/bottom of
content. Use cases include disabling the pull-to-refresh
feature on mobile, removing overscroll glow and rubberbanding effects,
and preventing page content from scrolling when it's beneath a modal/overlay.

overscroll-behavior requires Chrome 63+. It's in development or being
considered by other browsers. See
chromestatus.com for
more information.

Background

Scroll boundaries and scroll chaining

Scroll chaining on Chrome Android.

Scrolling is one of the most fundamental ways to interact with a page, but
certain UX patterns can be tricky to deal with because of the browser's quirky
default behaviors. As an example, take an app drawer with a large number of
items that the user may have to scroll through. When they reach the bottom, the
overflow container stops scrolling because there's no more content to consume.
In other words, the user
reaches a "scroll boundary". But notice what happens if the user continues to
scroll. The content behind the drawer starts scrolling! Scrolling is
taken over by the parent container; the main page itself in the example.

Turns out this behavior is called scroll chaining; the browser's default
behavior when scrolling content. Oftentimes the default is pretty nice, but
sometimes it's not desirable or even unexpected. Certain apps may want to
provide a different user experience when the user hits a scroll boundary.

The pull-to-refresh effect

Pull-to-refresh is an intuitive gesture popularized by mobile apps such as
Facebook and Twitter. Pulling down on a social feed and releasing creates new
space for more recent posts to be loaded. In fact, this particular UX has
become so popular that mobile browsers like Chrome on Android have adopted
the same effect. Swiping down at the top of the page refreshes the entire page:

For situations like the Twitter PWA,
it might make sense to disable the native pull-to-refresh action. Why? In this
app, you probably don't want the user accidentally refreshing the page. There's
also the potential to see a double refresh animation! Alternatively, it might
be nicer to customize the browser's action, aligning it more closely to the site's
branding. The unfortunate part is that this type of customization has been
tricky to pull off. Developers end up writing unnecessary JavaScript, add
non-passive
touch listeners (which block scrolling), or stick the entire page in a 100vw/vh
<div> (to prevent the page from overflowing). These workarounds have
well-documented negative
effects on scrolling performance.

We can do better!

Introducing overscroll-behavior

The overscroll-behaviorproperty is a new CSS feature that controls
the behavior of what happens when you over-scroll a container (including the
page itself). You can use it to cancel scroll chaining, disable/customize the
pull-to-refresh action, disable rubberbanding effects on iOS (when Safari
implements overscroll-behavior), and more.
The best part is that using overscroll-behavior does not adversely affect
page performance like the hacks mentioned in the intro!

The property takes three possible values:

auto - Default. Scrolls that originate on the element may propagate to
ancestor elements.

contain - prevents scroll chaining. Scrolls do not propagate to ancestors
but local effects within the node are shown. For example, the overscroll glow
effect on Android or the rubberbanding effect on iOS which notifies the user
when they've hit a scroll boundary. Note: using
overscroll-behavior: contain on the html element prevents overscroll
navigation actions.

none - same as contain but it also prevents overscroll effects within
the node itself (e.g. Android overscroll glow or iOS rubberbanding).

Note:overscroll-behavior also supports shorthands for overscroll-behavior-x
and overscroll-behavior-y if you only want to define behaviors for a certain
axis.

Let's dive into some examples to see how to use overscroll-behavior.

Prevent scrolls from escaping a fixed position element

The chatbox scenario

Content beneath the chat window scrolls too :(

Consider a fixed positioned chatbox that sits at the bottom of the page. The
intention is that the chatbox is a self-contained component and that it scrolls
separately from the content behind it. However, because of scroll chaining, the
document starts scrolling as soon as the user hits the last message in the chat
history.

For this app, it's more appropriate to have scrolls that originate within the
chatbox stay within the chat. We can make that happen by adding
overscroll-behavior: contain to the element that holds the chat messages:

Essentially, we're creating a logical separation between the chatbox's scrolling
context and the main page. The end result is that the main page stays put when
the user reaches the top/bottom of the chat history. Scrolls that start in the
chatbox do not propagate out.

The page overlay scenario

Another variation of the "underscroll" scenario is when you see content
scrolling behind a fixed position overlay. A dead giveaway
overscroll-behavior is in order! The browser is trying to be helpful but
it ends up making the site look buggy.

Example - modal with and without overscroll-behavior: contain:

Before: page content scrolls beneath overlay.

After: page content doesn't scroll beneath overlay.

Disabling pull-to-refresh

Turning off the pull-to-refresh action is a single line of CSS. Just prevent
scroll chaining on the entire viewport-defining element. In most cases, that's
<html> or <body>:

With this simple addition, we fix the double pull-to-refresh animations in
the chatbox demo and can
instead, implement a custom effect which uses a neater loading animation. The
entire inbox also blurs as the inbox refreshes:

Note: This will still preserve left/right swipe navigations. To prevent
navigations, you can use overscroll-behavior-x: none. However, this is still
being implemented in Chrome.

Full demo

Putting it all together, the full
chatbox demo uses
overscroll-behavior to create a custom pull-to-refresh animation
and disable scrolls from escaping the chatbox widget. This provides an optimal
user experience that would have been tricky to achieve without CSS
overscroll-behavior.