iOS 10 Safari: Prevent scrolling behind a fixed overlay and maintain scroll position

75,388

Solution 1

Add -webkit-overflow-scrolling: touch; to the #overlay element.

Then add this JavaScript code at the end of the body tag:

(function () {
  var _overlay = document.getElementById('overlay');
  var _clientY = null; // remember Y position on touch start

  _overlay.addEventListener('touchstart', function (event) {
    if (event.targetTouches.length === 1) {
      // detect single touch
      _clientY = event.targetTouches[0].clientY;
    }
  }, false);

  _overlay.addEventListener('touchmove', function (event) {
    if (event.targetTouches.length === 1) {
      // detect single touch
      disableRubberBand(event);
    }
  }, false);

  function disableRubberBand(event) {
    var clientY = event.targetTouches[0].clientY - _clientY;

    if (_overlay.scrollTop === 0 && clientY > 0) {
      // element is at the top of its scroll
      event.preventDefault();
    }

    if (isOverlayTotallyScrolled() && clientY < 0) {
      //element is at the top of its scroll
      event.preventDefault();
    }
  }

  function isOverlayTotallyScrolled() {
    // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#Problems_and_solutions
    return _overlay.scrollHeight - _overlay.scrollTop <= _overlay.clientHeight;
  }
}())

Solution 2

Combined Bohdan Didukh's approach with my previous approach to create an easy to use npm package to disable/enable body scroll.

https://github.com/willmcpo/body-scroll-lock

For more details on how the solution works, read https://medium.com/jsdownunder/locking-body-scroll-for-all-devices-22def9615177

Solution 3

I was trying to find a clean solution to this for a long time, and what seems to have worked best for me is setting pointer-events: none; on the body, and then pointer-events: auto; explicitly on the item I want to allow scrolling in.

Solution 4

Bohdan's solution above is great. However, it doesn't catch/block the momentum -- i.e. the case when user is not at the exact top of the page, but near the top of the page (say, scrollTop being 5px) and all of a sudden the user does a sudden massive pull down! Bohand's solution catches the touchmove events, but since -webkit-overflow-scrolling is momentum based, the momentum itself can cause extra scrolling, which in my case was hiding the header and was really annoying.

Why is it happening?

In fact, -webkit-overflow-scrolling: touch is a double-purpose property.

  1. The good purpose is it gives the rubberband smooth scrolling effect, which is almost necessary in custom overflow:scrolling container elements on iOS devices.
  2. The unwanted purpose however is this "oversrolling". Which is kinda making sense given it's all about being smooth and not sudden stops! :)

Momentum-blocking Solution

The solution I came up with for myself was adapted from Bohdan's solution, but instead of blocking touchmove events, I am changing the aforementioned CSS attribute.

Just pass the element that has overflow: scroll (and -webkit-overflow-scrolling: touch) to this function at the mount/render time.

The return value of this function should be called at the destroy/beforeDestroy time.

const disableOverscroll = function(el: HTMLElement) {
    function _onScroll() {
        const isOverscroll = (el.scrollTop < 0) || (el.scrollTop > el.scrollHeight - el.clientHeight);
        el.style.webkitOverflowScrolling = (isOverscroll) ? "auto" : "touch";
        //or we could have: el.style.overflow = (isOverscroll) ? "hidden" : "auto";
    }

    function _listen() {
        el.addEventListener("scroll", _onScroll, true);
    }

    function _unlisten() {
        el.removeEventListener("scroll", _onScroll);
    }

    _listen();
    return _unlisten();
}

Quick short solution

Or, if you don't care about unlistening (which is not advised), a shorter answer is:

el = document.getElementById("overlay");
el.addEventListener("scroll", function {
    const isOverscroll = (el.scrollTop < 0) || (el.scrollTop > el.scrollHeight - el.clientHeight);
    el.style.webkitOverflowScrolling = (isOverscroll) ? "auto" : "touch";
}, true);

Solution 5

Simply changing the overflow scrolling behavior on the body worked for me:

body {
    -webkit-overflow-scrolling: touch;
}
Share:
75,388
Gavin
Author by

Gavin

Updated on July 09, 2022

Comments

  • Gavin
    Gavin almost 2 years

    I'm not able to prevent the main body content from scrolling while a fixed position overlay is showing. Similar questions have been asked many times, but all of the techniques that previously worked do not seem to work on Safari in iOS 10. This seems like a recent issue.

    Some notes:

    • I can disable scrolling if I set both html and body to overflow: hidden, however that makes the body content scroll to the top.
    • If the content in the overlay is long enough so that it can be scrolled, scrolling is correctly disabled for the main page content. If the content in the overlay is not long enough to cause scrolling, you can scroll the main page content.
    • I included a javascript function from https://blog.christoffer.online/2015-06-10-six-things-i-learnt-about-ios-rubberband-overflow-scrolling/ that disables touchmove while the overlay is showing. This worked previously, but no longer works.

    Here's the full HTML source:

    <!doctype html>
    <html>
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
        <style type="text/css">
            html, body {
                width: 100%;
                height: 100%;
                margin: 0;
                padding: 0;
            }
            body {
                font-family: arial;
            }
            #overlay {
                display: none;
                position: fixed;
                z-index: 9999;
                left: 0;
                right: 0;
                top: 0;
                bottom: 0;
                overflow: scroll;
                color: #fff;
                background: rgba(0, 0, 0, 0.5);
            }
            #overlay span {
                position: absolute;
                display: block;
                right: 10px;
                top: 10px;
                font-weight: bold;
                font-size: 44px;
                cursor: pointer;
            }
            #overlay p {
                display: block;
                padding: 100px;
                font-size: 36px;
            }
            #page {
                width: 100%;
                height: 100%;
            }
            a {
                font-weight: bold;
                color: blue;
            }
        </style>
        <script>
            $(function() {
                $('a').click(function(e) {
                    e.preventDefault();
                    $('body').css('overflow', 'hidden');
                    $('#page').addClass('disable-scrolling'); // for touchmove technique below
    
                    $('#overlay').fadeIn();
                });
                $('#overlay span').click(function() {
                    $('body').css('overflow', 'auto');
                    $('#page').removeClass('disable-scrolling'); // for touchmove technique below
    
                    $('#overlay').fadeOut();
                });
            });
    
            /* Technique from http://blog.christoffer.me/six-things-i-learnt-about-ios-safaris-rubber-band-scrolling/ */
            document.ontouchmove = function ( event ) {
                var isTouchMoveAllowed = true, target = event.target;
                while ( target !== null ) {
                    if ( target.classList && target.classList.contains( 'disable-scrolling' ) ) {
                        isTouchMoveAllowed = false;
                        break;
                    }
                    target = target.parentNode;
                }
                if ( !isTouchMoveAllowed ) {
                    event.preventDefault();
                }
            };
        </script>
    </head>
    
    <body>
        <div id="overlay">
            <span>&times;</span>
            <p>fixed popover</p>
        </div>
    
        <div id="page">
            <strong>this is the top</strong><br>
            lots of scrollable content<br>
            asdfasdf<br>
            lots of scrollable content<br>
            asdfasdf<br>
            lots of scrollable content<br>
            asdfasdf<br>
            lots of scrollable content<br>
            asdfasdf<br>
            lots of scrollable content<br>
            asdfasdf<br>
            lots of scrollable content<br>
            asdfasdf<br>
            lots of scrollable content<br>
            asdfasdf<br>
            lots of scrollable content<br>
            asdfasdf<br>
            lots of scrollable content<br>
            asdfasdf<br>
            lots of scrollable content<br>
            asdfasdf<br>
            lots of scrollable content<br>
            asdfasdf<br>
            lots of scrollable content<br>
            asdfasdf<br>
            lots of scrollable content<br>
            asdfasdf<br>
            lots of scrollable content<br>
            asdfasdf<br>
            lots of scrollable content<br>
            asdfasdf<br>
            lots of scrollable content<br>
            asdfasdf<br>
            lots of scrollable content<br>
            asdfasdf<br>
            lots of scrollable content<br>
            asdfasdf<br>
            lots of scrollable content<br>
            asdfasdf<br>
            lots of scrollable content<br>
            asdfasdf<br>
            <br>
            <div><a href="#">Show Popover</a></div>
            <br>
            <br>
    
        </div>
    
    </body>
    
    </html>