Preventing body scroll for modals in iOS
TL;DR solution for 2020/iOS 13+
Thanks to the comments below (and that’s why I love comments so much), we can put a few things together to have a working CSS only solution for iOS 13+:
.bg-scrolling-element-when-modal-active {
/* when modal active */
touch-action: none;
-webkit-overflow-scrolling: none;
overflow: hidden;
/* Other browsers */
overscroll-behavior: none;
}
You can have a play with that here: https://codepen.io/benfrain/live/wvayeWq
I have left the rest of the post un-touched from 2016.
Original post from 2016 follows
touch-action
property. Sadly, iOS support is lacking at present: http://caniuse.com/#feat=css-touch-action. Want to add your weight to getting this feature implemented? Use https://bugs.webkit.org/show_bug.cgi?id=133112.Suppose you are building something that pops a modal window from time to time. This probably works well in most places, the problem is, on iOS, even if you toggle:
html,
body {
overflow: hidden;
}
iOS doesn’t prevent users from scrolling past the modal if there is content that exceeds the viewport height, despite you adding that condition to the CSS. One solution is to write the window.innerHeight
into both HTML and body elements in the DOM and then toggle the overflow: hidden
style on and off on the body
and html
:
var vpH = window.innerHeight;
document.documentElement.style.height = vpH.toString() + "px";
body.style.height = vpH.toString() + "px";
You can get a similar effect by setting the body
and html
to position: fixed
when you expose the modal and ditch the JavaScript there.However, neither of those solutions prevents users from doing the ‘elastic band’ thing at the bottom; and that subsequently reveals an ugly space. It would be nice if we had something better. My esteemed colleague, Tom suggested making use of touchstart
to prevent the default scroll behaviour and while that solved the initial problem (being able to scroll past the modal) it prevented clicks inside the modal. But it wasn’t long before that approach led us to using touchmove instead.
We can use it like this — at the same time that you invoke a function to make the attribute change or class change that shows the modal, you also do this:
stopBodyScrolling(true);
And when you want to allow scrolling again, you fire it like this:
stopBodyScrolling(false);
Behind the scenes we then need two functions:
function stopBodyScrolling (bool) {
if (bool === true) {
document.body.addEventListener("touchmove", freezeVp, false);
} else {
document.body.removeEventListener("touchmove", freezeVp, false);
}
}
The function receives either true or false and subsequently adds or removes an event listener. We then need a simple function reference that disables the default behaviour of the touchmove
event.
var freezeVp = function(e) {
e.preventDefault();
};
You can view a basic demo of this technique here: http://benfrain.com/playground/modal-demo.html.
You’ll need to do further work if your modal contains a scrolling section but for basic modals this seems to solve the issue quite nicely.
I’d like a simpler CSS solution but this is a pretty light-weight way to get the job done. I welcome a better approach if anyone knows one?
Could you share details about your implementation? How is the dialog positioned in CSS? Is there an overlay? Do you use the standard element and API (with polyfill, ofc)? I’ve found a few demos of dialogs, but neither of them prevent body scroll, so I’d be interested to see a demo that does prevent it.