DEV Community

Cover image for Handling Overscroll in a Web App with Header and Footer on iOS
Michael Puckett
Michael Puckett

Posted on • Updated on

Handling Overscroll in a Web App with Header and Footer on iOS

Introduction

Web pages, originally designed as documents, want to scroll. And since at least the iPhone, web pages can "overscroll" beyond their bounds, based on touch gesture inertia. In fact, every scrollable area on a web page viewed on an iPhone gets this extra "bouncy" scrolling behavior.

Nested scrollable areas "chain" with their parents (including the page itself), so that if you reach the top of a nested scrollable area, the closest scrollable parent will take over scrolling.

However, some app-like web experiences may wish to opt-out of this document-flow/scroll-chaining paradigm. They need to present a fixed scrollable main content area with a fixed header and footer, which goes against the browser's nature.

The main content area, if scrolled, could trigger the entire page to start scrolling unexpectedly. Or, if the page is fixed positioned, nothing will happen and the user will get "trapped" scrolling an unscrollable web page for a few seconds.

To opt out of this paradigm in any browser except Safari, you can use a new CSS property called overscroll-behavior. Setting overscroll-behavior: contain will prevent scroll chaining.

Handling iOS Safari

Polyfilling this CSS property in Safari is pretty tricky.

For non-scrollable elements, you can prevent scroll chaining by simply turning off touch gestures. You can do that with a CSS property that is supported by Safari: touch-action: none.

But for scrollable elements, JavaScript will be required.

Remember that scroll chaining occurs when you reach the bounds of the element. So we need to ensure that the user is never able to fully scroll to the top or bottom. Doing this the wrong way can cause UX problems, because the user will clearly be fighting against the default inertia scroll.

So here's the trick:

  1. Create an inner element that is at least 3px taller than the size of its scrolling parent, to force the area to get the overscroll behavior.
  2. Immediately set the scroll position to 1px to prevent scroll chaining when scrolling up
  3. With JavaScript, catch when the scroll position is exactly 0 or exactly at the bottom. After a requestAnimationFrame, set the scroll position to 1px from either the top or bottom.

The container will still get the inertia scroll (the user won't have to fight it) but it won't trigger scroll chaining.

Here's the JavaScript function I have:

this.addEventListener('scroll', async handleScroll() {
  await new Promise(resolve => window.requestAnimationFrame(resolve))
  const {
    scrollTop,
    scrollLeft,
    scrollHeight,
    clientHeight
  } = this
  const atTop = scrollTop === 0
  const beforeTop = 1
  const atBottom = scrollTop === scrollHeight - clientHeight
  const beforeBottom = scrollHeight - clientHeight - 1

  if (atTop) {
    this.scrollTo(scrollLeft, beforeTop) 
  } else if (atBottom) {
    this.scrollTo(scrollLeft, beforeBottom)
  }
}

Conclusion

Integrating this into an already-existing app probably won't be easy, since this may necessitate restructuring a lot of the page.

Hopefully Safari will implement the overscroll-behavior CSS property soon, so we can avoid this mess!

Here's the WebKit issue:

https://bugs.webkit.org/show_bug.cgi?id=176454

If you've ever faced this challenge, add yourself to the CC list on that issue to indicate that this is important to you.

Discussion (0)