Prevent body scrolling but allow overlay scrolling

CssOverlayLightbox

Css Problem Overview


I've been searching for a "lightbox" type solution that allows this but haven't found one yet (please, suggest if you know of any).

The behavior I'm trying to recreate is just like what you'd see at Pinterest when clicking on an image. The overlay is scrollable (as in the whole overlay moves up like a page on top of a page) but the body behind the overlay is fixed.

I attempted to create this with just CSS (i.e. a div overlay on top of the whole page and body with overflow: hidden), but it doesn't prevent div from being scrollable.

How to keep the body/page from scrolling but keep scrolling inside the fullscreen container?

Css Solutions


Solution 1 - Css

Theory

Looking at current implementation of the pinterest site (it might change in the future), when you open the overlay a noscroll class is applied to the body element and overflow: hidden is set, thus body is no longer scrollable.

The overlay (created on-the-fly or already inside the page and made visible via display: block, it makes no difference) has position : fixed and overflow-y: scroll, with top, left, right and bottom properties set to 0: this style makes the overlay fill the whole viewport.

The div inside the overlay is instead just in position: static then the vertical scrollbar you see is related to that element. As a result the content is scrollable but overlay remains fixed.

When you close the zoom you hide the overlay (via display: none) and then you could also entirely remove it via javascript (or just the content inside, it's up to you how to inject it).

As a final step you have to also remove the noscroll class to the body (so the overflow property returns to its initial value)


Code

> Codepen Example

(it works by changing the aria-hidden attribute of the overlay in order to show and hide it and to increase its accessibility).

Markup
(open button)

<button type="button" class="open-overlay">OPEN LAYER</button>

(overlay and close button)

<section class="overlay" aria-hidden="true">
  <div>
    <h2>Hello, I'm the overlayer</h2>
    ...   
    <button type="button" class="close-overlay">CLOSE LAYER</button>
  </div>
</section>

CSS

.noscroll { 
  overflow: hidden;
}

.overlay { 
   position: fixed; 
   overflow-y: scroll;
   top: 0; right: 0; bottom: 0; left: 0; }

[aria-hidden="true"]  { display: none; }
[aria-hidden="false"] { display: block; }

Javascript (vanilla-JS)

var body = document.body,
    overlay = document.querySelector('.overlay'),
    overlayBtts = document.querySelectorAll('button[class$="overlay"]');
    
[].forEach.call(overlayBtts, function(btt) {

  btt.addEventListener('click', function() { 
     
     /* Detect the button class name */
     var overlayOpen = this.className === 'open-overlay';
     
     /* Toggle the aria-hidden state on the overlay and the 
        no-scroll class on the body */
     overlay.setAttribute('aria-hidden', !overlayOpen);
     body.classList.toggle('noscroll', overlayOpen);
     
     /* On some mobile browser when the overlay was previously
        opened and scrolled, if you open it again it doesn't 
        reset its scrollTop property */
     overlay.scrollTop = 0;

  }, false);

});

Finally, here's another example in which the overlay opens with a fade-in effect by a CSS transition applied to the opacity property. Also a padding-right is applied to avoid a reflow on the underlying text when the scrollbar disappears.

> Codepen Example (fade)

CSS

.noscroll { overflow: hidden; }

@media (min-device-width: 1025px) {
    /* not strictly necessary, just an experiment for 
       this specific example and couldn't be necessary 
       at all on some browser */
    .noscroll { 
        padding-right: 15px;
    }
}

.overlay { 
     position: fixed; 
     overflow-y: scroll;
     top: 0; left: 0; right: 0; bottom: 0;
}

[aria-hidden="true"] {    
    transition: opacity 1s, z-index 0s 1s;
    width: 100vw;
    z-index: -1; 
    opacity: 0;  
}

[aria-hidden="false"] {  
    transition: opacity 1s;
    width: 100%;
    z-index: 1;  
    opacity: 1; 
}

Solution 2 - Css

If you want to prevent overscrolling on ios, you can add position fixed to your .noscroll class

body.noscroll{
    position:fixed;
    overflow:hidden;
}

Solution 3 - Css

overscroll-behavior css property allows to override the browser's default overflow scroll behavior when reaching the top/bottom of content.

Just add the following styles to overlay:

.overlay {
   overscroll-behavior: contain;
   ...
}

> Codepen demo

Currently works in Chrome, Firefox and IE(caniuse)

For more details check google developers article.

Solution 4 - Css

Don't use overflow: hidden; on body. It automatically scrolls everything to the top. There's no need for JavaScript either. Make use of overflow: auto;. This solution even works with mobile Safari:

HTML Structure

<div class="overlay">
    <div class="overlay-content"></div>
</div>

<div class="background-content">
    lengthy content here
</div>

Styling

.overlay{
    position: fixed;
    top: 0px;
    left: 0px;
    right: 0px;
    bottom: 0px;
    background-color: rgba(0, 0, 0, 0.8);

    .overlay-content {
        height: 100%;
        overflow: scroll;
    }
}

.background-content{
    height: 100%;
    overflow: auto;
}

See the demo here and source code here.

Update:

For people who want keyboard space bar, page up/down to work: you need to focus on the overlay, e.g., clicking on it, or manually JS focusing on it before this part of the div will respond to keyboard. Same with when the overlay is "switched off", since it's just moving the overlay to the side. Otherwise to browser, these are just two normal divs and it wouldn't know why it should focus on any one of them.

Solution 5 - Css

Most solutions have the problem that they do not retain the scroll position, so I took a look at how Facebook does it. In addition to setting the underlaying content to position: fixed they also set the top dynamically to retain the scroll position:

scrollPosition = window.pageYOffset;
mainEl.style.top = -scrollPosition + 'px';

Then, when you remove the overlay again, you need to reset the scroll position:

window.scrollTo(0, scrollPosition);

I created a little example to demonstrate this solution

let overlayShown = false;
let scrollPosition = 0;

document.querySelector('.toggle').addEventListener('click', function() {
  if (!overlayShown) {
		showOverlay();
  } else {
    removeOverlay();
  }
  overlayShown = !overlayShown;
});

function showOverlay() {
    scrollPosition = window.pageYOffset;
  	const mainEl = document.querySelector('.main-content');
    mainEl.style.top = -scrollPosition + 'px';
  	document.body.classList.add('show-overlay');
}

function removeOverlay() {
		document.body.classList.remove('show-overlay');
  	window.scrollTo(0, scrollPosition);
    const mainEl = document.querySelector('.main-content');
    mainEl.style.top = 0;
}

.main-content {
  background-image: repeating-linear-gradient( lime, blue 103px);
  width: 100%;
  height: 200vh;
}

.show-overlay .main-content {
  position: fixed;
  left: 0;
  right: 0;
  overflow-y: scroll; /* render disabled scroll bar to keep the same width */
/* Suggestion to put: overflow-y: hidden; 
Disabled scrolling still makes a mess with its width. Hiding it does the trick. */
}

.overlay {
  display: none;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.3);
  overflow: auto;
}

.show-overlay .overlay {
  display: block;
}

.overlay-content {
  margin: 50px;
  background-image: repeating-linear-gradient( grey, grey 20px, black 20px, black 40px);
  height: 120vh;
}

.toggle {
  position: fixed;
  top: 5px;
  left: 15px;
  padding: 10px;
  background: red;
}

/* reset CSS */
body {
  margin: 0;
}

<main class="main-content"></main>

  <div class="overlay">
    <div class="overlay-content"></div>
  </div>
  
  <button class="toggle">Overlay</button>

Solution 6 - Css

It is worth noting that sometimes adding "overflow:hidden" to the body tag doesn't do the job. In those cases, you'll have to add the property to the html tag as well.

html, body {
    overflow: hidden;
}

Solution 7 - Css

You can easily do this with some "new" css and JQuery.

Initially: body {... overflow:auto;} With JQuery you can dynamically switch between 'overlay' and 'body'. When on 'body', use

body {
   position: static;
   overflow: auto;
}

When on 'overlay' use

body {
   position: sticky;
   overflow: hidden;
}

JQuery for the switch('body'->'overlay'):

$("body").css({"position": "sticky", "overflow": "hidden"});

JQuery for the switch('overlay'->'body'):

$("body").css({"position": "static", "overflow": "auto"});

Solution 8 - Css

Generally speaking, if you want a parent (the body in this case) to prevent it from scrolling when a child (the overlay in this case) scrolls, then make the child a sibling of the parent to prevent the scroll event from bubbling up to the parent. In case of the parent being the body, this requires an additional wrapping element:

<div id="content">
</div>
<div id="overlay">
</div>

See Scroll particular DIV contents with browser's main scrollbar to see its working.

Solution 9 - Css

The chosen answer is correct, but has some limitations:

  • Super hard "flings" with your finger will still scroll <body> in the background
  • Opening the virtual keyboard by tapping an <input> in the modal will direct all future scrolls to <body>

I don't have a fix for the first issue, but wanted to shed some light on the second. Confusingly, Bootstrap used to have the keyboard issue documented, but they claimed it was fixed, citing http://output.jsbin.com/cacido/quiet as an example of the fix.

Indeed, that example works fine on iOS with my tests. However, upgrading it to the latest Bootstrap (v4) breaks it.

In an attempt to figure out what the difference between them was, I reduced a test case to no longer depend on Bootstrap, http://codepen.io/WestonThayer/pen/bgZxBG.

The deciding factors are bizarre. Avoiding the keyboard issue seems to require that background-color is not set on the root <div> containing the modal and the modal's content must be nested in another <div>, which can have background-color set.

To test it, uncomment the below line in the Codepen example:

.modal {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 2;
  display: none;
  overflow: hidden;
  -webkit-overflow-scrolling: touch;
  /* UNCOMMENT TO BREAK */
/*   background-color: white; */
}

Solution 10 - Css

The behaviour you want to prevent is called scroll chaining. To disable it, set

overscroll-behavior: contain;

on your overlay in CSS.

Solution 11 - Css

I found this question trying to solve issue I had with my page on Ipad and Iphone - body was scrolling when I was displaying fixed div as popup with image.

Some answers are good, however none of them solved my issue. I found following blog post by Christoffer Pettersson. Solution presented there helped issue I had with iOS devices and it helped my scrolling background problem.

Six things I learnt about iOS Safari's rubber band scrolling

As it was suggested I include major points of the blog post in case link gets outdated.

"In order to disable that the user can scroll the background page while the "menu is open", it is possible to control what elements should be allowed to be scrolled or not, by applying some JavaScript and a CSS class.

Based on this Stackoverflow answer you can control that elements with the disable-scrolling should not perform their default scroll action when the touchmove event is triggered."

 document.ontouchmove = function ( event ) {

    var isTouchMoveAllowed = true, target = event.target;

    while ( target !== null ) {
        if ( target.classList && target.classList.contains( 'disable-scrolling' ) ) {
            isTouchMoveAllowed = false;
            break;
        }
        target = target.parentNode;
    }

    if ( !isTouchMoveAllowed ) {
        event.preventDefault();
    }
};

And then put the disable-scrolling class on the page div:

<div class="page disable-scrolling">

Solution 12 - Css

For touch devices, try adding a 1px wide, 101vh min-height transparent div in the wrapper of the overlay. Then add -webkit-overflow-scrolling:touch; overflow-y: auto; to the wrapper. This tricks mobile safari into thinking the overlay is scrollable, thus intercepting the touch event from the body.

Here's a sample page. Open on mobile safari: http://www.originalfunction.com/overlay.html

https://gist.github.com/YarGnawh/90e0647f21b5fa78d2f678909673507f

Solution 13 - Css

if anyone is looking for a solution for React function components, you can put this inside the modal component:

 useEffect(() => {
    document.body.style.overflowY = 'hidden';
    return () =>{
      document.body.style.overflowY = 'auto';
    }
  }, [])

Solution 14 - Css

Simple inline styling for the body tag:

<body style="position: sticky; overflow: hidden;">

Solution 15 - Css

If the intent is to disable on mobile/ touch devices then the most straightforward way to do it is using touch-action: none;.

Example:

const app = document.getElementById('app');
const overlay = document.getElementById('overlay');

let body = '';

for (let index = 0; index < 500; index++) {
  body += index + '<br />';
}

app.innerHTML = body;
app.scrollTop = 200;

overlay.innerHTML = body;

* {
  margin: 0;
  padding: 0;
}

html,
body {
  height: 100%;
}

#app {
  background: #f00;
  position: absolute;
  height: 100%;
  width: 100%;
  overflow-y: scroll;
  line-height: 20px;
}

#overlay {
  background: rgba(0,0,0,.5);
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 100%;
  padding: 0 0 0 100px;
  overflow: scroll;
}

<div id='app'></div>
<div id='overlay'></div>

(The example does not work in the context of Stack Overflow. You will need to recreate it in a stand-alone page.)

If you want to disable scrolling of the #app container, just add touch-action: none;.

Solution 16 - Css

I'd like to add to previous answers because I tried to do that, and some layout broke as soon as I switched the body to position:fixed. In order to avoid that, I had to also set body's height to 100% :

function onMouseOverOverlay(over){
    document.getElementsByTagName("body")[0].style.overflowY = (over?"hidden":"scroll");
    document.getElementsByTagName("html")[0].style.position = (over?"fixed":"static");
    document.getElementsByTagName("html")[0].style.height = (over?"100%":"auto");
}

Solution 17 - Css

Use the following HTML:

<body>
  <div class="page">Page content here</div>
  <div class="overlay"></div>
</body>

Then JavaScript to intercept and stop scrolling:

$(".page").on("touchmove", function(event) {
  event.preventDefault()
});

Then to get things back to normal:

$(".page").off("touchmove");

Solution 18 - Css

try this

var mywindow = $('body'), navbarCollap = $('.navbar-collapse');    
navbarCollap.on('show.bs.collapse', function(x) {
                mywindow.css({visibility: 'hidden'});
                $('body').attr("scroll","no").attr("style", "overflow: hidden");
            });
            navbarCollap.on('hide.bs.collapse', function(x) {
                mywindow.css({visibility: 'visible'});
                $('body').attr("scroll","yes").attr("style", "");
            });

Solution 19 - Css

One solution for a React functional component is to use the useEffect hook.

Here's the code example bellow (pay attention to the useEffect definition):

import {useEffect, useRef} from "react";

export default function PopoverMenu({className, handleClose, children}) {
  const selfRef = useRef(undefined);

  useEffect(() => {
    const isPopoverOpenned = selfRef.current?.style.display !== "none";
    const focusedElement = document?.activeElement;
    const scrollPosition = {x: window.scrollX, y: window.scrollY};
    if (isPopoverOpenned) {
      preventDocBodyScrolling();
    } else {
      restoreDocBodyScrolling();
    }

    function preventDocBodyScrolling() {
      const width = document.body.clientWidth;
      const hasVerticalScrollBar = (window.innerWidth > document.documentElement.clientWidth);
      document.body.style.overflowX = "hidden";
      document.body.style.overflowY = hasVerticalScrollBar ? "scroll" : "";
      document.body.style.width = `${width}px`;
      document.body.style.position = "fixed";

    }

    function restoreDocBodyScrolling() {
      document.body.style.overflowX = "";
      document.body.style.overflowY = "";
      document.body.style.width = "";
      document.body.style.position = "";
      focusedElement?.focus();
      window.scrollTo(scrollPosition.x, scrollPosition.y);
    }


    return () => {
      restoreDocBodyScrolling(); // cleanup on unmount
    };
  }, []);

  return (
    <>
      <div
        className="backdrop"
        onClick={() => handleClose && handleClose()}
      />
      <div
        className={`pop-over-menu${className ? (` ${className}`) : ""}`}
        ref={selfRef}
      >
        <button
          className="pop-over-menu--close-button" type="button"
          onClick={() => handleClose && handleClose()}
        >
          X
        </button>
        {children}
      </div>
    </>
  );
}

Originally posted on this other related Stackoverflow question: https://stackoverflow.com/a/69016517/14131330

Solution 20 - Css

CSS
.noScroll {
    overflow: hidden;
}
Javascript
<script>
    function toggleNav() {
        document.body.classList.toggle("noScroll");
    }
</script>

Button

<button onclick="toggleNav()">
 Toggle Nav
</button>

Solution 21 - Css

In my case, none of these solutions worked out on iPhone (iOS 11.0).

The only effective fix that is working on all my devices is this one - ios-10-safari-prevent-scrolling-behind-a-fixed-overlay-and-maintain-scroll-position

Solution 22 - Css

If you want to stop body/html scroll add as the following

CSS

    html, body {
        height: 100%;
    }
                
    .overlay{
        position: fixed;
        top: 0px;
        left: 0px;
        right: 0px;
        bottom: 0px;
        background-color: rgba(0, 0, 0, 0.8);

        .overlay-content {
            height: 100%;
            overflow: scroll;
        }
    }
    
    .background-content{
        height: 100%;
        overflow: auto;
    }

HTML

    <div class="overlay">
        <div class="overlay-content"></div>
    </div>

    <div class="background-content">
        lengthy content here
    </div>

Basically, you could do it without JS.

The main idea is to add html/body with height: 100% and overflow: auto. and inside your overlay, you could either enable/disable scroll based on your requirement.

Hope this helps!

Solution 23 - Css

Use below code for disabling and enabling scroll bar.

Scroll = (
    function(){
          var x,y;
         function hndlr(){
            window.scrollTo(x,y);
            //return;
          }  
          return {

               disable : function(x1,y1){
                    x = x1;
                    y = y1;
                   if(window.addEventListener){
                       window.addEventListener("scroll",hndlr);
                   } 
                   else{
                        window.attachEvent("onscroll", hndlr);
                   }     

               },
               enable: function(){
                      if(window.removeEventListener){
                         window.removeEventListener("scroll",hndlr);
                      }
                      else{
                        window.detachEvent("onscroll", hndlr);
                      }
               } 

          }
    })();
 //for disabled scroll bar.
Scroll.disable(0,document.body.scrollTop);
//for enabled scroll bar.
Scroll.enable();

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionPotatoFroView Question on Stackoverflow
Solution 1 - CssFabrizio CalderanView Answer on Stackoverflow
Solution 2 - Cssam80lView Answer on Stackoverflow
Solution 3 - CssIgor AlemasowView Answer on Stackoverflow
Solution 4 - CssLuciaView Answer on Stackoverflow
Solution 5 - CssPhilipp MittererView Answer on Stackoverflow
Solution 6 - CssFrancisco HodgeView Answer on Stackoverflow
Solution 7 - CsschristkView Answer on Stackoverflow
Solution 8 - CssNGLNView Answer on Stackoverflow
Solution 9 - CssWestonView Answer on Stackoverflow
Solution 10 - Css黄雨伞View Answer on Stackoverflow
Solution 11 - CssTheFullResolutionView Answer on Stackoverflow
Solution 12 - CssYarGnawhView Answer on Stackoverflow
Solution 13 - CssMoeView Answer on Stackoverflow
Solution 14 - CssSharhabeel HamdanView Answer on Stackoverflow
Solution 15 - CssGajusView Answer on Stackoverflow
Solution 16 - CssVic SeedoubleyewView Answer on Stackoverflow
Solution 17 - CssDamir KotoricView Answer on Stackoverflow
Solution 18 - CssJunaidView Answer on Stackoverflow
Solution 19 - CssAlexandre DesrochesView Answer on Stackoverflow
Solution 20 - CsskelvinView Answer on Stackoverflow
Solution 21 - CsspotarView Answer on Stackoverflow
Solution 22 - CssAshwinView Answer on Stackoverflow
Solution 23 - CssRakesh ChaudhariView Answer on Stackoverflow