70Days

70 days since this post was last revised. It's possible some minor details may have changed.

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?

Ben Frain Developer, Author: 'Enduring CSS', 'Responsive Web Design with HTML5 & CSS3'.