238

I have a little "floating tool box" - a div with position:fixed; overflow:auto. Works just fine.

But when scrolling inside that box (with the mouse wheel) and reaching the bottom OR top, the parent element "takes over" the "scroll request" : The document behind the tool box scrolls.
- Which is annoying and not what the user "asked for".

I'm using jQuery and thought I could stop this behaviour with event.stoppropagation():
$("#toolBox").scroll( function(event){ event.stoppropagation() });

It does enter the function, but still, propagation happens anyway (the document scrolls)
- It's surprisingly hard to search for this topic on SO (and Google), so I have to ask:
How to prevent propagation / bubbling of the scroll-event ?

Edit:
Working solution thanks to amustill (and Brandon Aaron for the mousewheel-plugin here:
https://github.com/brandonaaron/jquery-mousewheel/raw/master/jquery.mousewheel.js

$(".ToolPage").bind('mousewheel', function(e, d)  
    var t = $(this);
    if (d > 0 && t.scrollTop() === 0) {
        e.preventDefault();
    }
    else {
        if (d < 0 && (t.scrollTop() == t.get(0).scrollHeight - t.innerHeight())) {
            e.preventDefault();
        }
    }
});
Pandaiolo
  • 11,165
  • 5
  • 38
  • 70
T4NK3R
  • 4,245
  • 3
  • 23
  • 25
  • 1
    Looks like it might not be possible. http://stackoverflow.com/questions/1459676/prevent-scroll-bubbling-from-element-to-window – musaul Apr 27 '11 at 10:35
  • 4
    @Musaul - actually that thread gave 2 possible solutions (if a bit rouge): setting `overflow:hidden` on the document, when hovering in the toolbox, or saving the documents scrollTop, and forcing it upon the document repeatedly (nice), during toolbox.scroll()... – T4NK3R Apr 27 '11 at 10:55
  • 1
    Yeah, I meant the scroll event bubbling. But I suppose it gives you workarounds. I'd completely avoid the scroll forcing option though. Doing too much (or anything in complex pages) in the scroll event can make the browser freeze for a while, especially on slower computers. – musaul Apr 27 '11 at 11:46
  • This works beautifully in everything other than IE, when attached to the body tag. With the above fix, it seems to disable mousewheel scrolling entirely. – Matthew Mar 28 '12 at 22:08
  • Please take a look at my answer, @Matthew. It resolves the IE issue, as well as normalizing for FireFox without any plug-ins. – Troy Alford Jun 13 '13 at 19:57
  • this is more than you asked for, and there are more than enough answers posted below, but I wrote a script a'la "hover intent" for jQuery, which tracks the user's [mouse-wheel intent](http://jsbin.com/ORoCozi/4/edit) – mindplay.dk Jan 09 '14 at 19:32
  • The solution at this question is far simpler and far better in most cases: http://stackoverflow.com/questions/10211203/scrolling-child-div-scrolls-the-window-how-do-i-stop-that – lots-to-learn Feb 19 '16 at 16:01
  • This doesn't work if user uses up and down arrow from the keyboard to scroll. – IfOnly Jul 21 '16 at 06:20
  • This question shouldn't have been closed - the "duplicate" question specifically asks for scrolling in an iframe and has answers just for dealing with an iframe. – Chris Hayes Oct 14 '21 at 16:56
  • Brandon Aaron's plugin link is 404 – Jayden Lawson Nov 05 '21 at 05:53

32 Answers32

175

I am adding this answer for completeness because the accepted answer by @amustill does not correctly solve the problem in Internet Explorer. Please see the comments in my original post for details. In addition, this solution does not require any plugins - only jQuery.

In essence, the code works by handling the mousewheel event. Each such event contains a wheelDelta equal to the number of px which it is going to move the scrollable area to. If this value is >0, then we are scrolling up. If the wheelDelta is <0 then we are scrolling down.

FireFox: FireFox uses DOMMouseScroll as the event, and populates originalEvent.detail, whose +/- is reversed from what is described above. It generally returns intervals of 3, while other browsers return scrolling in intervals of 120 (at least on my machine). To correct, we simply detect it and multiply by -40 to normalize.

@amustill's answer works by canceling the event if the <div>'s scrollable area is already either at the top or the bottom maximum position. However, Internet Explorer disregards the canceled event in situations where the delta is larger than the remaining scrollable space.

In other words, if you have a 200px tall <div> containing 500px of scrollable content, and the current scrollTop is 400, a mousewheel event which tells the browser to scroll 120px further will result in both the <div> and the <body> scrolling, because 400 + 120 > 500.

So - to solve the problem, we have to do something slightly different, as shown below:

The requisite jQuery code is:

$(document).on('DOMMouseScroll mousewheel', '.Scrollable', function(ev) {
    var $this = $(this),
        scrollTop = this.scrollTop,
        scrollHeight = this.scrollHeight,
        height = $this.innerHeight(),
        delta = (ev.type == 'DOMMouseScroll' ?
            ev.originalEvent.detail * -40 :
            ev.originalEvent.wheelDelta),
        up = delta > 0;

    var prevent = function() {
        ev.stopPropagation();
        ev.preventDefault();
        ev.returnValue = false;
        return false;
    }

    if (!up && -delta > scrollHeight - height - scrollTop) {
        // Scrolling down, but this will take us past the bottom.
        $this.scrollTop(scrollHeight);
        return prevent();
    } else if (up && delta > scrollTop) {
        // Scrolling up, but this will take us past the top.
        $this.scrollTop(0);
        return prevent();
    }
});

In essence, this code cancels any scrolling event which would create the unwanted edge condition, then uses jQuery to set the scrollTop of the <div> to either the maximum or minimum value, depending on which direction the mousewheel event was requesting.

Because the event is canceled entirely in either case, it never propagates to the body at all, and therefore solves the issue in IE, as well as all of the other browsers.

I have also put up a working example on jsFiddle.

Community
  • 1
  • 1
Troy Alford
  • 26,660
  • 10
  • 64
  • 82
  • 14
    This is, by far, the most comprehensive answer. I made your function into a jQuery extension, so it can be used inline in a jQuery object chain. See [this Gist](https://gist.github.com/theftprevention/5959411). – theftprevention Jul 09 '13 at 17:40
  • Nice - thanks for sharing that, and for the kudos. :) – Troy Alford Jul 10 '13 at 02:48
  • 2
    Excellent, comprehensive and easy to understand answer. – David Tuite Oct 21 '13 at 11:57
  • 2
    This works very well, but there seems to be an inertia problem when scrolling really fast. The page is still scrolled by about 20 pixels, which is not too bad. – juminoz Dec 20 '13 at 22:00
  • 1
    There should be `$this.scrollTop(scrollHeight - height);` for the first case. – Mitar Jan 27 '14 at 05:03
  • 1
    This worked out great for me. But for some reason it does not work on iFrames in IE. At least that's what it looks like to me. Here's a fiddle of the issue: http://jsfiddle.net/4wrxq/84/ I've experienced this issue in IE9 and IE10. It works fine in Chrome. – jkupczak Mar 11 '14 at 20:41
  • Tested in FF on a fixed position div, it stopped scrolling completely, even on target. – David D Jul 14 '14 at 16:13
  • Can you post a Fiddle of that behavior? I suspect that FF, which has a non-standard implementation compared to all other browsers I've tested with, reports mouse events differently over Fixed-Position elements. I'd like to confirm, though, that your scenario is, in fact, different than in Chrome or IE. – Troy Alford Jul 15 '14 at 15:20
  • 5
    Don't lock scrolling if the content doesn't overflow: `if (this.scrollHeight <= parseInt($(this).css("max-height"))) return` as the first line in the function. – T4NK3R Jul 25 '14 at 09:34
  • That's precisely what the `if`/`then` statement at the end does. Unless I'm misunderstanding, this addition would be superfluous. – Troy Alford Sep 02 '14 at 21:45
  • 2
    This worked great for me with a minor change. I found that setting the element's height to $this.height() wasn't responding properly to the bottom of an element if it had paddings/margins. So I changed it to $this.outerHeight(true). – Joseph Coco Sep 08 '14 at 22:28
  • What about touch move – Muhammad Umer Mar 21 '15 at 21:01
  • 1
    Example doesn't work when user scrolls via trackpad or via touch on touch-enabled devices. – smohadjer Jun 28 '15 at 11:52
  • If you don't understand the jsFiddle example, add more
  • elements to the outer container so it is given a scrollbar, too. My monitor was high enough so there wasn't any.
  • – wiktus239 Jul 09 '15 at 12:44
  • @T4NK3R It's not always desired - you still don't want your right-now-background elements to scroll when you use the possibly scrollable list. – wiktus239 Jul 09 '15 at 13:43
  • Must be careful when `.Scrollable` has set a `padding-top` or `padding-bottom`. You need to calculate into scroll down condition – twxia Apr 15 '16 at 06:32
  • Thank you! It worked nicely for me on Mac OS X, trackpad and Chrome. It worked only sometimes in Firefox. – Sleiman Jun 07 '16 at 15:20
  • does this also work on touch scrolling? – Mr.Moe Feb 28 '17 at 14:55
  • Sorry, Mr. Moe - I'm not sure, as I haven't tested it for that. (This answer was posted in 2013) – Troy Alford Mar 02 '17 at 23:11
  • 1
    Excellent.. Totally fixed my slimScroll. – Niall Murphy Apr 02 '17 at 04:18
  • 1
    Woot! Glad to hear it, @NiallMurphy! – Troy Alford Apr 12 '17 at 16:44
  • This solution doesn't work in Firefox. But if you change event to `wheel` and change the rest of the script to use `event.deltaY` instead of delta or wheelDelta then it would. Gist to demonstrate: https://gist.github.com/s-mage/bca26b996f193f94fdf789d8324c5c7b – Sergei Jun 13 '17 at 13:35