21

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?

3
  • 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. Commented Jun 8, 2016 at 22:48
  • Are you saying you can't "I can't save the scrollY and reset on close" or you don't know how. Commented Jun 9, 2016 at 0:15
  • 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! Commented Jun 9, 2016 at 18:39

9 Answers 9

20

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/

Sign up to request clarification or add additional context in comments.

7 Comments

The demo is buggy on ios 10. Sometimes you can scroll in the body but not the modal.
Recent finding: Put -webkit-overflow-scrolling: touch; to the overlay and get a nice touch scrolling on iOS.
Just tried (jerrylow.com/demo/ios-body-lock) - not working on ios 11.2. Body is scrolling :(
This didn't work completely for me, as the background (body) would still be scrolled down, I believe because of rubber banding. But then I added fixed positioning and I was able to stop the background from scrolling. Here's what I added to my "html, body" selector: position: fixed; top: 0; left: 0.
this does not work for modals with no scroll. It allows the user to scroll in the background
|
8

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>

1 Comment

This is the only thing on the internet that works for this issue
6

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.

2 Comments

iNoBounce.enable() is working perfectly as it disable body scrolling and retain the scrolling position. But I cannot iNoBounce.disable() it :D Thanks for the awesome work. ---- Update ---- iNoBounce.disable() is working now
Finally! Worked for me in 2019. God bless you :)
3

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

Comments

2

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.

1 Comment

Very neat solution (the simplest I have seen so far) ! although it creates a small stuttering effect in the end when it increments the scroll value by 1px.
2

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});
        });
    }
});

1 Comment

By disabling touchmove you disabling every possible scrolls in your app :(
1

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

Comments

0

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.)

Comments

0

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" />

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.