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

Update 27.4.17: if you are on a modern browser, this problem can be solved elegantly with the 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?

Learn to use CSS effectively, 'Enduring CSS' is out now

Get $5 off HERE ↠

Write and maintain large scale modular CSS and embrace modern tooling including PostCSS, Stylelint and Gulp