19
votes

I know there have been a few questions similar to this but they either don't work for my use case or the accepted answers have a flaw that doesn't work for me. So...

I have a page with a list of elements. Clicking on an element in the list will open an overlay with details about that element. I need that overlay to be scrollable but I don't want the rest of the page under the overlay to scroll so that once the overlay is closed you are in the same position (also the overlay is slightly transparent so it is annoying to the user to see the page scrolling below, also why I can't save the scrollY and reset on close).

Right now I have the working everywhere except iOS. This is basically what I have:

<html>
   <body>
      <ul id="list">
         <li>something 1</li>
         <li>something 2</li>
         <li>something 3</li>
         <li>something 4</li>
         <li>something 5</li>
      </ul>
      <div id="overlay"></div>
   </body>
</html>

CSS:

body.hidden {
   overflow: hidden;
}
#overlay {
   opacity: 0;
   top: -100vh;
}
#overlay.open {
   opacity: 1;
   overflow-y: scroll;
   overflow-x: hidden;
   top: 0;
}

Then in my click hander I toggle the hidden class on body, the open class on #overlay, and populate the #overlay element with my content. Like I said this works fine everywhere except for iOS.

Solutions I have seen other places say I need to use position:fixed and height:100% on the body and/or html tags. The problem with this solution is that you lose your scroll position and when you close the overlay you're back at the top of the page. Some of these lists can be really long so that isn't an option for me.

I can't prevent scrolling completely with preventDefault on body or something because I need the overlay content to be scrollable.

Any other suggestions?

9
I'd be eager to see a clean solution, but I had this same issue recently. I had to use Javascript to record the current scrollposition and restore it upon the unloading of the overlay. Might be all we can do until Safari gets its behavior in order with the rest of the web. - Conspicuous Compiler
Are you saying you can't "I can't save the scrollY and reset on close" or you don't know how. - jerrylow
Was saying I couldn't because the overlay was transparent and you could see behind that it had scrolled but using your answer below to offset with top fixes that as well. Thanks! - jcmitch

9 Answers

19
votes

There is no way around this right now. As of iOS 9.3 there's still no good way to prevent the scroll on the body. The best method that I currently implement on all sites that require it is to lock the html and the body's height and overflow.

html, body {
  height: 100%;
  overflow: hidden;
}

This is the best way to prevent iOS scroll on the content behind the overlay/modal.

Then to preserve the scroll position I shift the content behind up to look like its retaining it then when the modal closes restore the body's position.

I do this with a lock and unlock function in jQuery

var $docEl = $('html, body'),
  $wrap = $('.content'),
  $.scrollTop;

$.lockBody = function() {
  if(window.pageYOffset) {
    scrollTop = window.pageYOffset;

    $wrap.css({
      top: - (scrollTop)
    });
  }

  $docEl.css({
    height: "100%",
    overflow: "hidden"
  });
}

$.unlockBody = function() {
  $docEl.css({
    height: "",
    overflow: ""
  });

  $wrap.css({
    top: ''
  });

  window.scrollTo(0, scrollTop);
  window.setTimeout(function () {
    scrollTop = null;
  }, 0);
}

When you piece all these together you get http://codepen.io/jerrylow/pen/yJeyoG if you want to test it on your phone here's just the result: http://jerrylow.com/demo/ios-body-lock/

8
votes

Why does the page scroll when I'm scrolling on the modal?

If you have the css property -webkit-overflow-scrolling: touch; enabled on the element behind the modal, some native code kicks in that seems to listen for touchmove events which we are unable to capture.

So what now?

I've fixed this for my application by adding a class to negate the css property when the modal is visible. This is a fully working example.

let pageEl = document.querySelector(".page");
let modalEl = document.querySelector(".modal");

function openModal(e){
  e.preventDefault();
  pageEl.classList.add("page--has-modal");
  modalEl.classList.remove("hidden");
  window.addEventListener("wheel", preventScroll);
  window.addEventListener("touchmove", preventScroll);
}
function closeModal(e){
  e.preventDefault();
  pageEl.classList.remove("page--has-modal");
  modalEl.classList.add("hidden");
  
  window.removeEventListener("wheel", preventScroll);
  window.removeEventListener("touchmove", preventScroll);
}

window.addEventListener("click", function(){
  console.log(modalEl.scrollHeight);
  console.log(modalEl.clientHeight);
});

function preventScroll(e){
  if (!isDescendant(modalEl, e.target)){
    e.preventDefault();
    return false;
  }
  
  let modalTop = modalEl.scrollTop === 0;
  let modalBottom = modalEl.scrollTop === (modalEl.scrollHeight -      modalEl.clientHeight);
  
  if (modalTop && e.deltaY < 0){
    e.preventDefault();
  } else if (modalBottom && e.deltaY > 0){
    e.preventDefault();
  }
}

function isDescendant(parent, child) {
     var node = child.parentNode;
     while (node != null) {
         if (node == parent) {
             return true;
         }
         node = node.parentNode;
     }
     return false;
}
.page { 
  -webkit-overflow-scrolling: touch; 
}
.page--has-modal { 
  -webkit-overflow-scrolling: auto;  
}

.modal {
  position: absolute;
  top: 50px;
  left: 50px;
  right: 50px;
  bottom: 50px;
  background: #c0c0c0;
  padding: 50px;
  text-align: center;
  overflow: auto;
  -webkit-overflow-scrolling: auto; 
}
.hidden {
  display: none;
}
<div class="page">
<button onclick="openModal(event);">Open modal</button>
<p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Integer consequat sapien a lectus gravida euismod. Sed vitae nisl non odio viverra accumsan. Curabitur nisi neque, egestas sed, vulputate sit amet, luctus vitae, dolor. Cras lacus massa, sagittis ut, volutpat consequat, interdum a, nulla. Vivamus rhoncus molestie nulla. Ut porttitor turpis sit amet turpis. Nam suscipit, justo quis ullamcorper sagittis, mauris diam dictum elit, suscipit blandit ligula ante sit amet mauris. Integer id arcu. Aenean scelerisque. Sed a purus. Pellentesque nec nisl eget metus varius tempor. Curabitur tincidunt iaculis lectus. Aliquam molestie velit id urna. Suspendisse in ante ac nunc commodo placerat.</p>

<p>Morbi gravida posuere est. Fusce id augue. Sed facilisis, felis quis ornare consequat, neque risus faucibus dui, quis ullamcorper tellus lacus vitae felis. Phasellus ac dolor. Integer ante diam, consectetuer in, tempor vitae, volutpat in, enim. Integer diam felis, semper at, iaculis ut, suscipit quis, dolor. Vestibulum semper, velit et tincidunt vehicula, nisl risus eleifend ipsum, vel consectetuer enim dolor id magna. Praesent hendrerit urna ac lacus. Maecenas porttitor ipsum sed orci. In ac odio vel lorem tincidunt pellentesque. Nam tempor pulvinar turpis. Nunc in leo in libero ultricies interdum. Proin ut urna. Donec ultricies nunc dapibus justo. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Praesent vulputate, lectus pulvinar nonummy eleifend, sapien urna posuere metus, vel auctor risus odio eu augue. Cras vitae dolor. Phasellus dolor. Etiam enim. Donec erat felis, tincidunt quis, luctus in, faucibus at, est.</p>
<div class="modal hidden">
Hi there!
<button onclick="closeModal(event);">Close me</button>
<p>Morbi gravida posuere est. Fusce id augue. Sed facilisis, felis quis ornare consequat, neque risus faucibus dui, quis ullamcorper tellus lacus vitae felis. Phasellus ac dolor. Integer ante diam, consectetuer in, tempor vitae, volutpat in, enim. Integer diam felis, semper at, iaculis ut, suscipit quis, dolor. Vestibulum semper, velit et tincidunt vehicula, nisl risus eleifend ipsum, vel consectetuer enim dolor id magna. Praesent hendrerit urna ac lacus. Maecenas porttitor ipsum sed orci. In ac odio vel lorem tincidunt pellentesque. Nam tempor pulvinar turpis. Nunc in leo in libero ultricies interdum. Proin ut urna. Donec ultricies nunc dapibus justo. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Praesent vulputate, lectus pulvinar nonummy eleifend, sapien urna posuere metus, vel auctor risus odio eu augue. Cras vitae dolor. Phasellus dolor. Etiam enim. Donec erat felis, tincidunt quis, luctus in, faucibus at, est.</p>
</div>
</div>
6
votes

We faced this exact problem - and finally solved it using:

https://github.com/lazd/iNoBounce

One gotcha was immediately after the script loads we had to call iNoBounce.disable() as it was starting up enabled and thus preventing any scrolling behaviour.

3
votes

The best solution I found which also prevents background scrolling while you scroll to the top or end of the overlay (fixed element) using vanilla javascript:

// "fixed-element" is the class of the overlay (fixed element) what has "position: fixed"
// Call disableScroll() and enableScroll() to toggle

var freeze = function(e) {
  if (!document.getElementsByClassName("fixed-element")[0].contains(e.target)) {
    e.preventDefault();
  }
}

var disableScroll = function() {
  document.body.style.overflow = "hidden"; // Or toggle using class: document.body.className += "overflow-hidden-class";

  // Only accept touchmove from fixed-element
  document.addEventListener('touchmove', freeze, false);

  // Prevent background scrolling
  document.getElementsByClassName("fixed-element")[0].addEventListener("touchmove", function(e) {
    var top = this.scrollTop,
      totalScroll = this.scrollHeight,
      currentScroll = top + this.offsetHeight;

    if (top === 0 && currentScroll === totalScroll) {
      e.preventDefault();
    } else if (top === 0) {
      this.scrollTop = 1;
    } else if (currentScroll === totalScroll) {
      this.scrollTop = top - 1;
    }
  });
}

var enableScroll = function() {
  document.removeEventListener("touchmove", freeze);
  document.body.style.overflow = "";
}

Benefits:
1. Does not make body "fixed" while open overlay (fixed element), so the page doesn't scroll to top.
2. Prevents background scrolling with the fixed element.

See Gist

1
votes

it seems iOS will only scroll the body once the overlay reaches min or max scrolling. So, set the scrollTop of the overlay to 1 instead of zero, and detect the onscroll event (which on iOS is fired after scrolling ends) and if at max (app.scrollHeight - app.scrollTop - app.clientHeight < 1) set it to one pixel shorter. For example

    var overlay = document.getElementById('overlay');

    function onScroll() {
        if (overlay.scrollTop < 1) {
            overlay.scrollTop = 1;
        } else if (overlay.scrollHeight - overlay.scrollTop - overlay.clientHeight < 1)                         {
            overlay.scrollTop = overlay.scrollTop - 1;
        }
    }


    overlay.addEventListener('scroll', onScroll);

You might want to add a check and only attach the event if running in iOS.

0
votes

I found this question whilst looking for a solution to my very similar problem. Perhaps the solution I found to mine will shed some light here.

In my case the problem was how to prevent a scrollable container from scrolling when using a scrollable widget in that container (e.g. an HTML5 slider rotated vertically using css transform). The scrollable container is defined as 'overflow-y: scroll' using an id selector in CSS.

I tried first using a .scroll-lock class that had 'overflow-y: hidden' and I toggled this on/off on the scrollable container with touchstart and touchend event listeners on all scrollable widgets. It didn't work. Attempts to use the widgets resulted in container scrolling. Then I tried all manner of javascript solutions until I found the ones here, and got closer to solving it.

In my case, the problem was specificity rules. Class selectors are trumped by id selectors, so my toggled class could not override the default overflow setting on the container that was applied by id. When I applied 'overflow: hidden' using the style attribute directly on the container div everything worked fine.

const $scrollableWidget = $('.vertical-slider-container');
$scrollableWidget.on('touchstart',function (e) {
    console.log("Disabling scroll on content area");
    $(`#content`).css("overflow-y", "hidden");
});
$scrollableWidget.on('touchend',function (e) {
    console.log("Re-enabling scroll on content area");
    $(`#content`).removeAttr("style");
});

For full details see this fiddle. (Use a mobile browser to try it out.)

https://jsfiddle.net/daffinm/fwgnm7hs/12/

Hope this helps someone. (I spent far too long solving this.)

0
votes

I had similar problem. When opening modal window - overlay is scrollable on iOS devices.

After playing with css and touch events, I decided to do next (on iOS devices only):

  • before opening modal - save scroll position to variable and make body fixed
  • after closing modal - make body unfixed and restore scroll position
0
votes

There is a simple solution. It works for me. You can try.

$(document).ready(function(){
    if( navigator.userAgent.match(/iPhone|iPad|iPod/i) ) {
        function preventBehavior(e) {
            e.preventDefault(); 
        };

        $(document).on('show.bs.modal', () => {
            document.addEventListener("touchmove", preventBehavior, {passive: false});

        }).on('hidden.bs.modal', () => {
            document.removeEventListener("touchmove", preventBehavior, {passive: true});
        });
    }
});
0
votes

In addition to setting the position to be fixed and top to be 0...if you are using viewport meta code (see below) in your header, be sure that the three scale variables are set to 1.0:

<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, target-densityDpi=device-dpi" />