iOS 10 Safari: Prevent scrolling behind a fixed overlay and maintain scroll position
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.
- The good purpose is it gives the rubberband smooth scrolling effect, which is almost necessary in custom
overflow:scrolling
container elements on iOS devices. - 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 unlisten
ing (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;
}
Gavin
Updated on July 09, 2022Comments
-
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
andbody
tooverflow: 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>×</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>
- I can disable scrolling if I set both