99

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;
    });
});
  • This is stopping all scrolling where as I want scrolling to still be possible inside the container
  • This is also not deactivating on mouse leave

Any ideas, or better ways of doing this?

Krupesh Kotecha
  • 2,396
  • 3
  • 21
  • 40
Walrus
  • 19,801
  • 35
  • 121
  • 199

14 Answers14

169

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.

Šime Vidas
  • 182,163
  • 62
  • 281
  • 385
  • Sort of: Why did you had var delta and this.scrollTop lines, they appear unnecessary – Walrus Sep 30 '11 at 14:49
  • @RobinKnight As I said, I scroll the DIV manually. You do wan't the Div to be scrollable, right? – Šime Vidas Sep 30 '11 at 14:59
  • Yes but the div is scrollable without those lines – Walrus Sep 30 '11 at 15:05
  • 2
    @RobinKnight Nope, doesn't seem so. Here is the working demo: http://jsfiddle.net/XNwbt/1/ and here is the demo with those lines removed: http://jsfiddle.net/XNwbt/2/ – Šime Vidas Sep 30 '11 at 15:35
  • 1
    Your manual scrolling is backwards on Firefox 10, at least on Linux and Mac. Seems to work correctly if you make that `-e.detail`, tested in Firefox (Mac, Linux), Safari (Mac), and Chromium (Linux). – Anomie Apr 05 '12 at 16:25
  • 1
    @Anomie That is correct. The sign of Mozilla's `.detail` does not correspond to the sign of `.wheelData` (which is what Chrome/IE have), so we have to invert it manually. Thanks for fixing my answer. – Šime Vidas Apr 05 '12 at 19:07
  • 8
    I used this, thank! I needed to add this tho: `if( e.originalEvent ) e = e.originalEvent;` – Nikso Jun 29 '12 at 09:26
  • thnx @Nikso, this fixed my problem (chrome + OSX), delta had been NaN previously. – Nick Briz Jul 17 '12 at 15:17
  • Mate that worked like a charm for a jquery coverflow embed that was scrolling both the page and the scrollable content. Thank you. – ekerner Aug 24 '12 at 12:46
  • @ŠimeVidas sadly this isn't working correctly with newer versions of jQuery (I think version>=1.7.x), even when replacing `.bind` with `.on`. The `.scrollable` elem is locked from scrolling too. Any idea what was changed in jQuery API that might affect it? – Nadir Sampaoli Nov 06 '13 at 08:59
  • 1
    @NadirSampaoli The property for both `wheelDelta` and `detail` are now nested under `originalEvent`. The respective third line change should look like this: `var delta = e.originalEvent.wheelDelta || -e.originalEvent.detail;` – user0000001 Dec 10 '13 at 18:41
  • 1
    What's the `30`? Sounds like a Magic Number... Is it 2em? My scroll is 100px, that's a big difference. – Rudie Dec 11 '13 at 00:47
  • @Rudie Yes, it's a magic number. Not sure if it's feasible (or even possible) to detect the browser's current native scroll delta in a reliable cross-browser fashion. – Šime Vidas Dec 11 '13 at 01:15
  • 1
    [Isn't this a better way then?](http://jsfiddle.net/rudiedirkx/F8qSq/show/) Making xbrowser shouldn't be hard. – Rudie Dec 11 '13 at 02:50
  • @Rudie Yes, it's better. Put it in an answer and I can refer to it from my answer. – Šime Vidas Dec 11 '13 at 05:00
  • 2
    @ŠimeVidas I made an adjustment to this after I implemented it in my software. Not critical but possibly add a sanity check for the scroll property to prevent the deadlock scrolling when the element has no scroll. `if ( $(this)[0].scrollHeight !== $(this).outerHeight() ) { //yourcode }` will execute only when there is a scrollbar. – user0000001 Dec 12 '13 at 16:42
  • I have found a lot solutions, but this one is simple, easy to implement and easy to extend. Great solution! Thanks a lot. – Jelmer Dec 22 '13 at 15:41
  • My sole purpose is to kiss you, Mr. Šime Vidas. – Edgar Froes Jan 12 '15 at 20:39
  • Something VERY interesting happened and I don't understand how. I added this snippet to my code, changed the selected to an overflowing div that I have (I specified just one), and now scrolling is disabled on the body, but *magically* ( didn't specify the other divs I have) ALL of the overflowing divs I have scroll beautifully. Can someone please explain this phenomena to me? – mila Jun 14 '15 at 07:51
  • @mila Make a demo and I can debug. – Šime Vidas Jun 14 '15 at 14:01
  • @ŠimeVidas I've decided to prevent scrolling altogether on the page and switch to ng-views. But thank you! – mila Jun 15 '15 at 12:15
  • how do i implement the same functionality for devices(tablets/phones) – usr30911 Jul 16 '15 at 19:52
  • @ViVekH Seems to work just fine: http://jsbin.com/ragequ/quiet (I’ve tested in Safari/iOS) – Šime Vidas Jul 16 '15 at 19:59
  • @ŠimeVidas for touch devices we dont have mousescroll so this doesnt work , we need to bind for touchmove , im searching snippet for that – usr30911 Jul 16 '15 at 20:02
  • @ViVekH When I swipe within the yellow element, the page is not scrolled. Please test on your touch devices and report your findings. – Šime Vidas Jul 16 '15 at 20:03
  • im testing on samsung galaxy tab 10 inch / chrome/ android and on scrolling the yellow element the page scrolls too – usr30911 Jul 16 '15 at 20:05
  • @ViVekH I can confirm. So, in Safari, not scrolling the page while swiping within the element just happens to be the default, and we need a JavaScript snippet which would implement that behavior on Android, too. – Šime Vidas Jul 16 '15 at 20:09
  • I am using the code as is and it's working perfectly in IE11 and FF 40, but doesn't work correctly in Chrome 44. It does enable the scrolling of the layer div, but the body (of the main page, behind the layer) also scrolls, unlike in your jsfiddle demo where it works properly even in Chrome 44. Any idea why it doesn't work correctly in Chrome on my site? By the way, I am using jQuery 1.11.3 – Nikita 웃 Aug 19 '15 at 15:21
  • Solved. I needed to add e.stopPropagation(); after e.preventDefault(); for it to properly work in Chrome – Nikita 웃 Aug 28 '15 at 06:18
  • Today I tried to use datetimepicker on the same page with your code and...it breaks this function...not working. What a wonderfull world of JS plugins...damn it :/ I noticed that internally DateTime plugin uses "jQuery Mouse Wheel Plugin"... which probably changes default mouse wheel capture by this function, or something else and everything breaks..damn it. – Andrew Oct 13 '16 at 20:03
  • @Andrew Whoa, that seems pretty intrusive. You should be able to use the standard `addEventListener` method to bind the mouse wheel event. – Šime Vidas Oct 14 '16 at 04:05
  • @ŠimeVidas do you have any example of how it might look like? Thanks – Andrew Oct 14 '16 at 06:00
  • @Andrew You’d need to start with something like this https://jsbin.com/zupuro/edit?js,output, and then figure out which events to bind in order to make it work in all browsers you care about. I couldn't get the original code to work in Firefox, so you'd definitely have to debug. – Šime Vidas Oct 14 '16 at 17:13
  • 1
    I tried this but I don't like the feel. The scrolling doesn't track well with scroll speed changes. The native implementation I think has acceleration support for the "wheel" (I am testing with 2 finger touch on a laptop), so very slow scrolling is supposed to barely move, while fast scrolling should move larger increments. With this solution the scrolling div scrolls at the same rate regardless. – johnb003 Jan 18 '17 at 01:20
30

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();
wojcikstefan
  • 867
  • 9
  • 7
  • 2
    Thanks! I think its more appropriate to not overwrite default behavior. To cross-browser support it can be used [jQuery Mouse Wheel](https://github.com/brandonaaron/jquery-mousewheel) plugin. – Meettya Jun 02 '13 at 17:57
  • Unfortunately this seems glitchy in Chrome 52 (OSX 10.10). Works perfectly in Safari, though. – frosty Aug 22 '16 at 05:02
  • 1
    Thank you! The other solutions made the scrolling super slow and buggy – agrublev May 18 '17 at 01:11
  • @wojcikstefan, I am getting this warning in chrome: `[Violation] Added non-passive event listener to a scroll-blocking 'mousewheel' event. Consider marking event handler as 'passive' to make the page more responsive.` – Syed Jul 27 '17 at 18:23
27

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

Prasoon Kumar
  • 279
  • 3
  • 2
21

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

Rudie
  • 52,220
  • 42
  • 131
  • 173
  • 2
    how do i implement the same functionality for devices(tablets/phones) i guess touchmove is the binding event – usr30911 Jul 16 '15 at 19:51
3

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.

3

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;
};
  • This works pretty well except when you crash into the top or bottom of the div the first event carries into the body. Testing with 2-finger touch on chrome in osx. – johnb003 Jan 18 '17 at 01:25
3

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/

3

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;
           });
       }
    });
Ricardo Binns
  • 3,228
  • 6
  • 44
  • 71
2

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
David D
  • 1,269
  • 17
  • 22
  • 2
    Please consider including some information about your answer, rather than simply posting code. We try to provide not just 'fixes', but help people learn. You should explain what was wrong in the original code, what you did differently, and why your change(s) worked. – Andrew Barber Aug 26 '14 at 19:08
1

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.

Ricky
  • 2,912
  • 8
  • 47
  • 74
0

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.

srcspider
  • 10,977
  • 5
  • 40
  • 35
0

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.

Carl Smith
  • 3,025
  • 24
  • 36
0

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

MR_AMDEV
  • 1,712
  • 2
  • 21
  • 38
0

All you need is

e.preventDefault();

on child element.