Walrus
Walrus

Reputation: 20444

How to prevent page scrolling when scrolling a DIV element?

I have reviewed and tested the various functions for preventing the body ability to scroll whilst inside a div and have combined a function that should work.

$('.scrollable').mouseenter(function() {
    $('body').bind('mousewheel DOMMouseScroll', function() {
        return false;
    });
    $(this).bind('mousewheel DOMMouseScroll', function() {
        return true;
    });
});
$('.scrollable').mouseleave(function() {
    $('body').bind('mousewheel DOMMouseScroll', function() {
        return true;
    });
});

Any ideas, or better ways of doing this?

Upvotes: 100

Views: 136623

Answers (16)

Walrus
Walrus

Reputation: 20444

2024 with wide browser support, CSS only

As of 2024 this can be accomplished entirely in CSS with wide browser support using the property overscroll-behavior. https://developer.mozilla.org/en-US/docs/Web/CSS/overscroll-behavior

Setting this property to contain will prevent scrolling of the parent or the window entirely and maintain the scroll dynamic utilised by the browser.

Browser support is almost unanimous. https://caniuse.com/?search=overscroll-behavior

I would post a demo but the link to Mozilla perfectly demonstrates it. Set the following property to the scrollable div:

overscroll-behavior: contain;

That's all folks!

Upvotes: 2

Kamal Zaitar
Kamal Zaitar

Reputation: 186

const area = document.getElementsByClassName('area')[0];

area.addEventListener("wheel", (e)=>{

    const deltaY = e.deltaY;

    const scrollAmount = 250; // Adjust this value as needed

    area.scrollBy({
        left: deltaY < 0 ? scrollAmount : -scrollAmount,
        // behavior: 'smooth'
    });
    
    e.preventDefault();

})
body{
    height: 200dvh;
    overflow: auto;
    background-color: red;
    display: flex;
    align-items: center;
    justify-content: center;
}

.area{
    overflow: auto;
    height: 50dvh;
    width: 100dvw;
    background-color: blue;
}

.sub_area{

    height: 50dvh;
    width: 200dvw;
    background-color: skyblue;
}
<div class="area">

      <div class="sub_area"></div>

  </div>

Upvotes: 1

Ayberk Baytok
Ayberk Baytok

Reputation: 79

All you need is

e.preventDefault();

on child element.

Upvotes: 0

Prasoon Kumar
Prasoon Kumar

Reputation: 359

Use below CSS property overscroll-behavior: contain; to child element

Upvotes: 35

MR_AMDEV
MR_AMDEV

Reputation: 1922

Here is the plugin that is useful for preventing parent scroll while scrolling a specific div and has a bunch of options to play with.

Check it out here:

https://github.com/MohammadYounes/jquery-scrollLock

Usage

Trigger Scroll Lock via JavaScript:

$('#target').scrollLock();

Trigger Scroll Lock via Markup:

    <!-- HTML -->
<div data-scrollLock
     data-strict='true'
     data-selector='.child'
     data-animation='{"top":"top locked","bottom":"bottom locked"}'
     data-keyboard='{"tabindex":0}'
     data-unblock='.inner'>
     ...
</div>

<!-- JavaScript -->
<script type="text/javascript">
  $('[data-scrollLock]').scrollLock()
</script>

View Demo

Upvotes: 0

Rudie
Rudie

Reputation: 53831

I think it's possible to cancel the mousescroll event sometimes: http://jsfiddle.net/rudiedirkx/F8qSq/show/

$elem.on('wheel', function(e) {
    var d = e.originalEvent.deltaY,
        dir = d < 0 ? 'up' : 'down',
        stop = (dir == 'up' && this.scrollTop == 0) || 
               (dir == 'down' && this.scrollTop == this.scrollHeight-this.offsetHeight);
    stop && e.preventDefault();
});

Inside the event handler, you'll need to know:

  • scrolling direction
    d = e.originalEvent.deltaY, dir = d < 0 ? 'up' : 'down' because a positive number means scrolling down
  • scroll position
    scrollTop for top, scrollHeight - scrollTop - offsetHeight for bottom

If you're

  • scrolling up, and top = 0, or
  • scrolling down, and bottom = 0,

cancel the event: e.preventDefault() (and maybe even e.stopPropagation()).

I think it's better to not override the browser's scrolling behaviour. Only cancel it when applicable.

It's probablt not perfectly xbrowser, but it can't be very hard. Maybe Mac's dual scroll direction is tricky though...

Upvotes: 21

Ricky
Ricky

Reputation: 3072

I needed to add this event to multiple elements that might have a scrollbar. For the cases where no scrollbar was present, the main scrollbar didn't work as it should. So i made a small change to @Šime code as follows:

$( '.scrollable' ).on( 'mousewheel DOMMouseScroll', function ( e ) {
    if($(this).prop('scrollHeight') > $(this).height())
    {
        var e0 = e.originalEvent, delta = e0.wheelDelta || -e0.detail;

        this.scrollTop += ( delta < 0 ? 1 : -1 ) * 30;
        e.preventDefault();
    }       
});

Now, only elements with a scrollbar will prevent the main scroll from begin stopped.

Upvotes: 1

Šime Vidas
Šime Vidas

Reputation: 185933

Update 2: My solution is based on disabling the browser's native scrolling altogether (when cursor is inside the DIV) and then manually scrolling the DIV with JavaScript (by setting its .scrollTop property). An alternative and IMO better approach would be to only selectively disable the browser's scrolling in order to prevent the page scroll, but not the DIV scroll. Check out Rudie's answer below which demonstrates this solution.


Here you go:

$( '.scrollable' ).on( 'mousewheel DOMMouseScroll', function ( e ) {
    var e0 = e.originalEvent,
        delta = e0.wheelDelta || -e0.detail;

    this.scrollTop += ( delta < 0 ? 1 : -1 ) * 30;
    e.preventDefault();
});

Live demo: https://jsbin.com/howojuq/edit?js,output

So you manually set the scroll position and then just prevent the default behavior (which would be to scroll the DIV or whole web-page).

Update 1: As Chris noted in the comments below, in newer versions of jQuery, the delta information is nested within the .originalEvent object, i.e. jQuery does not expose it in its custom Event object anymore and we have to retrieve it from the native Event object instead.

Upvotes: 170

here a simple solution without jQuery which does not destroy the browser native scroll (this is: no artificial/ugly scrolling):

var scrollable = document.querySelector('.scrollable');

scrollable.addEventListener('wheel', function(event) {
    var deltaY = event.deltaY;
    var contentHeight = scrollable.scrollHeight;
    var visibleHeight = scrollable.offsetHeight;
    var scrollTop = scrollable.scrollTop;

    if (scrollTop === 0 && deltaY < 0)
        event.preventDefault();
    else if (visibleHeight + scrollTop === contentHeight && deltaY > 0)
        event.preventDefault();
});

Live demo: http://jsfiddle.net/ibcaliax/bwmzfmq7/4/

Upvotes: 3

Vladimir Kuzmin
Vladimir Kuzmin

Reputation: 51

In the solution above there is a little mistake regarding Firefox. In Firefox "DOMMouseScroll" event has no e.detail property,to get this property you should write the following 'e.originalEvent.detail'.

Here is a working solution for Firefox:

$.fn.isolatedScroll = function() {
    this.on('mousewheel DOMMouseScroll', function (e) {
        var delta = e.wheelDelta || (e.originalEvent && e.originalEvent.wheelDelta) || -e.originalEvent.detail,
            bottomOverflow = (this.scrollTop + $(this).outerHeight() - this.scrollHeight) >= 0,
            topOverflow = this.scrollTop <= 0;

        if ((delta < 0 && bottomOverflow) || (delta > 0 && topOverflow)) {
            e.preventDefault();
        }
    });
    return this;
};

Upvotes: 3

Carl Younger
Carl Younger

Reputation: 3070

You can do this without JavaScript. You can set the style on both divs to position: fixed and overflow-y: auto. You may need to make one of them higher than the other by setting its z-index (if they overlap).

Here's a basic example on CodePen.

Upvotes: 0

srcspider
srcspider

Reputation: 11205

Pure javascript version of Vidas's answer, el$ is the DOM node of the plane you are scrolling.

function onlyScrollElement(event, el$) {
    var delta = event.wheelDelta || -event.detail;
    el$.scrollTop += (delta < 0 ? 1 : -1) * 10;
    event.preventDefault();
}

Make sure you dont attach the even multiple times! Here is an example,

var ul$ = document.getElementById('yo-list');
// IE9, Chrome, Safari, Opera
ul$.removeEventListener('mousewheel', onlyScrollElement);
ul$.addEventListener('mousewheel', onlyScrollElement);
// Firefox
ul$.removeEventListener('DOMMouseScroll', onlyScrollElement);
ul$.addEventListener('DOMMouseScroll', onlyScrollElement);

Word of caution, the function there needs to be a constant, if you reinitialize the function each time before attaching it, ie. var func = function (...) the removeEventListener will not work.

Upvotes: 0

David D
David D

Reputation: 1295

Here is my solution I've used in applications.

I disabled the body overflow and placed the entire website html inside container div's. The website containers have overflow and therefore the user may scroll the page as expected.

I then created a sibling div (#Prevent) with a higher z-index that covers the entire website. Since #Prevent has a higher z-index, it overlaps the website container. When #Prevent is visible the mouse is no longer hovering the website containers, so scrolling isn't possible.

You may of course place another div, such as your modal, with a higher z-index than #Prevent in the markup. This allows you to create pop-up windows that don't suffer from scrolling issues.

This solution is better because it doesn't hide the scrollbars (jumping affect). It doesn't require event listeners and it's easy to implement. It works in all browsers, although with IE7 & 8 you have to play around (depends on your specific code).

html

<body>
  <div id="YourModal" style="display:none;"></div>
  <div id="Prevent" style="display:none;"></div>
  <div id="WebsiteContainer">
     <div id="Website">
     website goes here...
     </div>
  </div>
</body>

css

body { overflow: hidden; }

#YourModal {
 z-index:200;
 /* modal styles here */
}

#Prevent {
 z-index:100;
 position:absolute;
 left:0px;
 height:100%;
 width:100%;
 background:transparent;
}

#WebsiteContainer {
  z-index:50;
  overflow:auto;
  position: absolute;
  height:100%;
  width:100%;
}
#Website {
  position:relative;
}

jquery/js

function PreventScroll(A) { 
  switch (A) {
    case 'on': $('#Prevent').show(); break;
    case 'off': $('#Prevent').hide(); break;
  }
}

disable/enable the scroll

PreventScroll('on'); // prevent scrolling
PreventScroll('off'); // allow scrolling

Upvotes: 2

Alexandru Lighezan
Alexandru Lighezan

Reputation: 112

A less hacky solution, in my opinion is to set overflow hidden on the body when you mouse over the scrollable div. This will prevent the body from scrolling, but an unwanted "jumping" effect will occur. The following solution works around that:

jQuery(".scrollable")
    .mouseenter(function(e) {
        // get body width now
        var body_width = jQuery("body").width();
        // set overflow hidden on body. this will prevent it scrolling
        jQuery("body").css("overflow", "hidden"); 
        // get new body width. no scrollbar now, so it will be bigger
        var new_body_width = jQuery("body").width();
        // set the difference between new width and old width as padding to prevent jumps                                     
        jQuery("body").css("padding-right", (new_body_width - body_width)+"px");
    })
    .mouseleave(function(e) {
        jQuery("body").css({
            overflow: "auto",
            padding-right: "0px"
        });
    })

You could make your code smarter if needed. For example, you could test if the body already has a padding and if yes, add the new padding to that.

Upvotes: 3

wojcikstefan
wojcikstefan

Reputation: 887

If you don't care about the compatibility with older IE versions (< 8), you could make a custom jQuery plugin and then call it on the overflowing element.

This solution has an advantage over the one Šime Vidas proposed, as it doesn't overwrite the scrolling behavior - it just blocks it when appropriate.

$.fn.isolatedScroll = function() {
    this.bind('mousewheel DOMMouseScroll', function (e) {
        var delta = e.wheelDelta || (e.originalEvent && e.originalEvent.wheelDelta) || -e.detail,
            bottomOverflow = this.scrollTop + $(this).outerHeight() - this.scrollHeight >= 0,
            topOverflow = this.scrollTop <= 0;

        if ((delta < 0 && bottomOverflow) || (delta > 0 && topOverflow)) {
            e.preventDefault();
        }
    });
    return this;
};

$('.scrollable').isolatedScroll();

Upvotes: 30

Ricardo Binns
Ricardo Binns

Reputation: 3246

see if this help you:

demo: jsfiddle

$('#notscroll').bind('mousewheel', function() {
     return false
});

edit:

try this:

    $("body").delegate("div.scrollable","mouseover mouseout", function(e){
       if(e.type === "mouseover"){
           $('body').bind('mousewheel',function(){
               return false;
           });
       }else if(e.type === "mouseout"){
           $('body').bind('mousewheel',function(){
               return true;
           });
       }
    });

Upvotes: 3

Related Questions