Prevent scrolling of parent element when inner element scroll position reaches top/bottom?
Asked Answered
S

32

244

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();
        }
    }
});
Scathing answered 27/4, 2011 at 10:13 Comment(10)
Looks like it might not be possible. #1460176Cardiff
@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()...Scathing
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.Cardiff
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.Rudnick
Please take a look at my answer, @Matthew. It resolves the IE issue, as well as normalizing for FireFox without any plug-ins.Cudlip
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 intentAdvocation
The solution at this question is far simpler and far better in most cases: #10211703Servitude
This doesn't work if user uses up and down arrow from the keyboard to scroll.Unbolted
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.Shirt
Brandon Aaron's plugin link is 404Phytobiology
E
45

It's possible with the use of Brandon Aaron's Mousewheel plugin.

Here's a demo: http://jsbin.com/jivutakama/edit?html,js,output

$(function() {

  var toolbox = $('#toolbox'),
      height = toolbox.height(),
      scrollHeight = toolbox.get(0).scrollHeight;

  toolbox.bind('mousewheel', function(e, d) {
    if((this.scrollTop === (scrollHeight - height) && d < 0) || (this.scrollTop === 0 && d > 0)) {
      e.preventDefault();
    }
  });

});
Electrical answered 27/4, 2011 at 10:53 Comment(18)
YES! It works in both FF and Chrome, but not (at all) in Opera for some reason..Scathing
Got it - in opera the d (direction) is -0.666 or 0.666 instead of -1 or 1) - so checking for d<0 or d>0 fixes it : )Scathing
I've issued a fix that now works in Opera. jsbin.com/ixura3/3Electrical
Great minds. That's exactly what my fix does.Electrical
This works for trapping the event, although I did find a minor issue with the condition to trigger preventDefault: for some elements like <ul> the "scrollTop" at the bottom of the list is off by a few (four) pixels consistently in different browsers. I may be doing something else wrong, but I found I could work around by modifying the condition - instead of "scrollHeight - height == this.scrollTop", I used "scrollHeight - height - scrollTop < 5".Serigraph
Whoops, those 4 pixels are accounted for in the difference between innerHeight.Serigraph
There's also some weirdness if you have scrolling inertia on and scroll a child container and then move your mouse out of the child. The inertia caries onto the parent or any other container. I experienced this on a magic mouse in chrome on OSX LionMetalinguistics
Works well in FF, IE. It works using the scrollwheel in chrome, but if you use the arrow keys then scroll events still bubble up to the parent.Bova
If you have dynamic content under your div this code is not working use this instead $('#divImageMain').bind('mousewheel', function(e, d) { if((this.scrollTop === (this.scrollHeight - $(this).height()) && d < 0) || (this.scrollTop === 0 && d > 0)) { e.preventDefault(); } });Elohim
the jsbin examples do not prevent page scrolling on an iPad.Thanatos
@Thanatos I'm aware of this issue. I hope to have time to revise the answer for all browsers and device.Electrical
The jsbin example is not working for me on an osx trackpadHutcherson
Not working for me on Mac OS X and Chrome 32.Charcoal
Not working on Windows 7 with Chrome 39Repentant
It is not working because the mousewheel code is loaded as 'text/plain'. If you place the code directly inside the script tag it will works. Yet please read the answer below by Troy Alford. And accepted answer != best answer.Malpighi
The demo provided at output.jsbin.com/ixura3/3 is not working anymore (Chrome 45)Sabellian
This of course only works on a desktop with a device that fires mousewheel events. If you switch to a touch device, the the scrolling using touch gestures will scroll the parent when the child is scrolled to its boundary.Kaleidoscope
for d < 0 use abs(st - (sh - $(scrollable).height())) < 1 to avoid subpixel problemsTrophozoite
C
177

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.

Cudlip answered 1/5, 2013 at 19:12 Comment(22)
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.Lichenin
Nice - thanks for sharing that, and for the kudos. :)Cudlip
Excellent, comprehensive and easy to understand answer.Apocrine
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.Kainite
There should be $this.scrollTop(scrollHeight - height); for the first case.Charcoal
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: jsfiddle.net/4wrxq/84 I've experienced this issue in IE9 and IE10. It works fine in Chrome.Sterol
Tested in FF on a fixed position div, it stopped scrolling completely, even on target.Surveyor
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.Cudlip
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.Scathing
That's precisely what the if/then statement at the end does. Unless I'm misunderstanding, this addition would be superfluous.Cudlip
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).Winfrid
What about touch moveRochellrochella
Example doesn't work when user scrolls via trackpad or via touch on touch-enabled devices.Coleorhiza
If you don't understand the jsFiddle example, add more <li> elements to the outer container so it is given a scrollbar, too. My monitor was high enough so there wasn't any.Youmans
@Scathing It's not always desired - you still don't want your right-now-background elements to scroll when you use the possibly scrollable list.Youmans
Must be careful when .Scrollable has set a padding-top or padding-bottom. You need to calculate into scroll down conditionSextillion
Thank you! It worked nicely for me on Mac OS X, trackpad and Chrome. It worked only sometimes in Firefox.Proceleusmatic
does this also work on touch scrolling?Stephie
Sorry, Mr. Moe - I'm not sure, as I haven't tested it for that. (This answer was posted in 2013)Cudlip
Excellent.. Totally fixed my slimScroll.Agrimony
Woot! Glad to hear it, @NiallMurphy!Cudlip
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: gist.github.com/s-mage/bca26b996f193f94fdf789d8324c5c7bLarimore
B
65

TL;DR: You can safely use overscroll-behavior: contain to achieve this on modern browsers.


All the solutions given in this thread don't mention an existing - and native - way to solve this problem without reordering DOM and/or using event preventing tricks. But there's a good reason: this way is proprietary - and available on MS web platform only. Quoting MSDN:

-ms-scroll-chaining property - specifies the scrolling behavior that occurs when a user hits the scroll limit during a manipulation. Property values:

chained - Initial value. The nearest scrollable parent element begins scrolling when the user hits a scroll limit during a manipulation. No bounce effect is shown.

none - A bounce effect is shown when the user hits a scroll limit during a manipulation.

Granted, this property is supported on IE10+/Edge only. Still, here's a telling quote:

To give you a sense of how popular preventing scroll chaining may be, according to my quick http-archive search "-ms-scroll-chaining: none" is used in 0.4% of top 300K pages despite being limited in functionality and only supported on IE/Edge.

And now good news, everyone! Starting from Chrome 63, we finally have a native cure for Blink-based platforms too - and that's both Chrome (obviously) and Android WebView (soon).

Quoting the introducing article:

The overscroll-behavior property is a new CSS feature that controls the behavior of what happens when you over-scroll a container (including the page itself). You can use it to cancel scroll chaining, disable/customize the pull-to-refresh action, disable rubberbanding effects on iOS (when Safari implements overscroll-behavior), and more.[...]

The property takes three possible values:

auto - Default. Scrolls that originate on the element may propagate to ancestor elements.

contain - prevents scroll chaining. Scrolls do not propagate to ancestors but local effects within the node are shown. For example, the overscroll glow effect on Android or the rubberbanding effect on iOS which notifies the user when they've hit a scroll boundary. Note: using overscroll-behavior: contain on the html element prevents overscroll navigation actions.

none - same as contain but it also prevents overscroll effects within the node itself (e.g. Android overscroll glow or iOS rubberbanding).

[...] The best part is that using overscroll-behavior does not adversely affect page performance like the hacks mentioned in the intro!

Here's this feature in action. And here's corresponding CSS Module document.

UPDATE: Firefox, since version 59, has joined the club, and MS Edge is expected to implement this feature in version 18. Here's the corresponding caniusage.

UPDATE 2: And now (Oct, 2022) Safari officially joined the club: since 16.0 version, overscroll-behavior is no longer behind the feature flag.

Bename answered 3/1, 2018 at 19:40 Comment(6)
caniuse.com/#search=overscroll-behavior I guess it will get better in the futurePhotoelectric
Safari doesn't support overscroll-behavior.Sybille
This strategy doesn’t seem to be effective on an inner element that doesn’t itself have enough content to be scrollable.Jerlenejermain
Since Safari 14.1, the feature is behind a flag and can be enabled in the "Experimental Features" developer menu. So most probably, they will enable this feature some time in the future ...Acidforming
@Bename Since this property has good support now I recommend cleaning up this answer to make it more clearArbutus
However I like this approach, - it has an inconsistency, and no way around it. Namely, if the area is "scrollable" but does not have enough content for the scrollbar to show up, the event isn't prevented from popping. For some, it might be desirable, sure, but there is certainly a number of scenarios where consistency is preferable.Counterpoise
E
45

It's possible with the use of Brandon Aaron's Mousewheel plugin.

Here's a demo: http://jsbin.com/jivutakama/edit?html,js,output

$(function() {

  var toolbox = $('#toolbox'),
      height = toolbox.height(),
      scrollHeight = toolbox.get(0).scrollHeight;

  toolbox.bind('mousewheel', function(e, d) {
    if((this.scrollTop === (scrollHeight - height) && d < 0) || (this.scrollTop === 0 && d > 0)) {
      e.preventDefault();
    }
  });

});
Electrical answered 27/4, 2011 at 10:53 Comment(18)
YES! It works in both FF and Chrome, but not (at all) in Opera for some reason..Scathing
Got it - in opera the d (direction) is -0.666 or 0.666 instead of -1 or 1) - so checking for d<0 or d>0 fixes it : )Scathing
I've issued a fix that now works in Opera. jsbin.com/ixura3/3Electrical
Great minds. That's exactly what my fix does.Electrical
This works for trapping the event, although I did find a minor issue with the condition to trigger preventDefault: for some elements like <ul> the "scrollTop" at the bottom of the list is off by a few (four) pixels consistently in different browsers. I may be doing something else wrong, but I found I could work around by modifying the condition - instead of "scrollHeight - height == this.scrollTop", I used "scrollHeight - height - scrollTop < 5".Serigraph
Whoops, those 4 pixels are accounted for in the difference between innerHeight.Serigraph
There's also some weirdness if you have scrolling inertia on and scroll a child container and then move your mouse out of the child. The inertia caries onto the parent or any other container. I experienced this on a magic mouse in chrome on OSX LionMetalinguistics
Works well in FF, IE. It works using the scrollwheel in chrome, but if you use the arrow keys then scroll events still bubble up to the parent.Bova
If you have dynamic content under your div this code is not working use this instead $('#divImageMain').bind('mousewheel', function(e, d) { if((this.scrollTop === (this.scrollHeight - $(this).height()) && d < 0) || (this.scrollTop === 0 && d > 0)) { e.preventDefault(); } });Elohim
the jsbin examples do not prevent page scrolling on an iPad.Thanatos
@Thanatos I'm aware of this issue. I hope to have time to revise the answer for all browsers and device.Electrical
The jsbin example is not working for me on an osx trackpadHutcherson
Not working for me on Mac OS X and Chrome 32.Charcoal
Not working on Windows 7 with Chrome 39Repentant
It is not working because the mousewheel code is loaded as 'text/plain'. If you place the code directly inside the script tag it will works. Yet please read the answer below by Troy Alford. And accepted answer != best answer.Malpighi
The demo provided at output.jsbin.com/ixura3/3 is not working anymore (Chrome 45)Sabellian
This of course only works on a desktop with a device that fires mousewheel events. If you switch to a touch device, the the scrolling using touch gestures will scroll the parent when the child is scrolled to its boundary.Kaleidoscope
for d < 0 use abs(st - (sh - $(scrollable).height())) < 1 to avoid subpixel problemsTrophozoite
S
28

I know it's quite an old question, but since this is one of top results in google... I had to somehow cancel scroll bubbling without jQuery and this code works for me:

function preventDefault(e) {
  e = e || window.event;
  if (e.preventDefault)
    e.preventDefault();
  e.returnValue = false;  
}

document.getElementById('a').onmousewheel = function(e) { 
  document.getElementById('a').scrollTop -= e. wheelDeltaY; 
  preventDefault(e);
}
Sharpset answered 20/3, 2012 at 11:58 Comment(4)
interesting idea, ..the mousewheel event is not cross-browser though, and the scroll distance needs to be normalised, see https://mcmap.net/q/119136/-normalizing-mousewheel-speed-across-browsersOccur
We can have nested views, which makes it buggy.Clannish
Does this work with mobile touch? ThanksNaucratis
@Naucratis definitelly not, because touch devices won't be firing mousewheel events unless you'd actually be using a mouse on them. Otherwise they do the normal scrolling. You might be lucky (ab)using touch events though...Kaleidoscope
D
19

EDIT: CodePen example

For AngularJS, I defined the following directive:

module.directive('isolateScrolling', function () {
  return {
    restrict: 'A',
      link: function (scope, element, attr) {
        element.bind('DOMMouseScroll', function (e) {
          if (e.detail > 0 && this.clientHeight + this.scrollTop == this.scrollHeight) {
            this.scrollTop = this.scrollHeight - this.clientHeight;
            e.stopPropagation();
            e.preventDefault();
            return false;
          }
          else if (e.detail < 0 && this.scrollTop <= 0) {
            this.scrollTop = 0;
            e.stopPropagation();
            e.preventDefault();
            return false;
          }
        });
        element.bind('mousewheel', function (e) {
          if (e.deltaY > 0 && this.clientHeight + this.scrollTop >= this.scrollHeight) {
            this.scrollTop = this.scrollHeight - this.clientHeight;
            e.stopPropagation();
            e.preventDefault();
            return false;
          }
          else if (e.deltaY < 0 && this.scrollTop <= 0) {
            this.scrollTop = 0;
            e.stopPropagation();
            e.preventDefault();
            return false;
          }

          return true;
        });
      }
  };
});

And then added it to the scrollable element (the dropdown-menu ul):

<div class="dropdown">
  <button type="button" class="btn dropdown-toggle">Rename <span class="caret"></span></button>
  <ul class="dropdown-menu" isolate-scrolling>
    <li ng-repeat="s in savedSettings | objectToArray | orderBy:'name' track by s.name">
      <a ng-click="renameSettings(s.name)">{{s.name}}</a>
    </li>
  </ul>
</div>

Tested on Chrome and Firefox. Chrome's smooth scrolling defeats this hack when a large mousewheel movement is made near (but not at) the top or bottom of the scroll region.

Dezhnev answered 2/1, 2014 at 23:51 Comment(14)
This does not appear to be working in Chrome 34. The binding is firing, but the page continues to scroll when the <ul> reaches the bottom.Adenocarcinoma
It's working for me in Chrome 36.Dezhnev
Doesn't work on Firefox or IE though, and neither it does after you add DOMMouseScroll.Elanaeland
it works for me, but the values are inverted ;) Just change > 0 with < 0 and vice versa. Tested on Windows 8.1, with chrome, firefox and ie11Efthim
I'm wondering if that has to do with a change in how the origin of the element is represented? The code assumes that the origin is at the top.Dezhnev
I've discovered that this doesn't fully work when Chrome's smooth scrolling is enabled. Since that causes the scrollTop to be animated over time, when the wheel event is processed the scrollTop doesn't appear to be at the top or bottom of the scroll region, so it lets the event through.Dezhnev
I've added Firefox support, tested with FF 39.Dezhnev
Works perfect, but for me it doesn't work if I use name 'isolateScrolling' but 'isolatescrolling' works =) Thanks for life save. Also I've added some costom code to detect if my dynamic ul has scroll, and if has then prevent DOM scroll. if (element.prop('offsetHeight') < element.prop('scrollHeight')) { e.stopPropagation(); e.preventDefault(); return false; }Followup
working, only difference I use is change e.deltaY for e.originalEvent.deltaY not sure why... why you use differents binds instead a single function? worked for me... tested on FF chrome safariAthene
@Athene As I recall, the event attributes differ between FF and Chrome. Are you saying that FF and Safari trigger the mousewheel event and not DOMMouseScroll?Dezhnev
codepen.io/anon/pen/yejxXM -- tested with Chrome 47 and Firefox 41.Dezhnev
Any interest in putting this on Github and creating a package on npm/bower?Richey
Do you have it supported in further version of angular like 6 or 7.Rally
sorry, I don't use newer versions of angularDezhnev
A
13

There are tons of questions like this out there, with many answers, but I could not find a satisfactory solution that did not involve events, scripts, plugins, etc. I wanted to keep it straight in HTML and CSS. I finally found a solution that worked, although it involved restructuring the markup to break the event chain.


1. Basic problem

Scrolling input (i.e.: mousewheel) applied to the modal element will spill over into an ancestor element and scroll it in the same direction, if some such element is scrollable:

(All examples are meant to be viewed on desktop resolutions)

https://jsfiddle.net/ybkbg26c/5/

HTML:

<div id="parent">
  <div id="modal">
    This text is pretty long here.  Hope fully, we will get some scroll bars.
  </div>
</div>

CSS:

#modal {
  position: absolute;
  height: 100px;
  width: 100px;
  top: 20%;
  left: 20%;
  overflow-y: scroll;
}
#parent {
  height: 4000px;
}

2. No parent scroll on modal scroll

The reason why the ancestor ends up scrolling is because the scroll event bubbles and some element on the chain is able to handle it. A way to stop that is to make sure none of the elements on the chain know how to handle the scroll. In terms of our example, we can refactor the tree to move the modal out of the parent element. For obscure reasons, it is not enough to keep the parent and the modal DOM siblings; the parent must be wrapped by another element that establishes a new stacking context. An absolutely positioned wrapper around the parent can do the trick.

The result we get is that as long as the modal receives the scroll event, the event will not bubble to the "parent" element.

It should typically be possible to redesign the DOM tree to support this behavior without affecting what the end user sees.

https://jsfiddle.net/0bqq31Lv/3/

HTML:

<div id="context">
  <div id="parent">
  </div>
</div>
<div id="modal">
  This text is pretty long here.  Hope fully, we will get some scroll bars.
</div>

CSS (new only):

#context {
  position: absolute;
  overflow-y: scroll;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
}

3. No scroll anywhere except in modal while it is up

The solution above still allows the parent to receive scroll events, as long as they are not intercepted by the modal window (i.e. if triggered by mousewheel while the cursor is not over the modal). This is sometimes undesirable and we may want to forbid all background scrolling while the modal is up. To do that, we need to insert an extra stacking context that spans the whole viewport behind the modal. We can do that by displaying an absolutely positioned overlay, which can be fully transparent if necessary (but not visibility:hidden).

https://jsfiddle.net/0bqq31Lv/2/

HTML:

<div id="context">
  <div id="parent">
  </div>
</div>
<div id="overlay">  
</div>
<div id="modal">
  This text is pretty long here.  Hope fully, we will get some scroll bars.
</div>

CSS (new on top of #2):

#overlay {
  background-color: transparent;
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
}
Armor answered 24/12, 2015 at 10:12 Comment(4)
The basic problem arises when the "modal" IS the child of another scrollable element - scripting seems unavoidable.. In my original code the toolbox was just one of several - each in a separate tab (COULD probably be moved to <body> - but that would be BIG)Scathing
@Scathing Yes, there are tradeoffs. In my case refactoring the DOM was by far preferable to throwing UI javascript onto the problem. Overall, I think this is an option worth contemplating for someone who designs their tree from scratch.Armor
This is the only approach that effectively avoids the issue instead of trying to fix a problem with tons of JS that could have been avoided from the start.Earthshaking
I've read dozens of answers to this issue, many of them with 100+ points. This is the only one that actually makes sense.Hillell
C
12

Here's a plain JavaScript version:

function scroll(e) {
  var delta = (e.type === "mousewheel") ? e.wheelDelta : e.detail * -40;
  if (delta < 0 && (this.scrollHeight - this.offsetHeight - this.scrollTop) <= 0) {
    this.scrollTop = this.scrollHeight;
    e.preventDefault();
  } else if (delta > 0 && delta > this.scrollTop) {
    this.scrollTop = 0;
    e.preventDefault();
  }
}
document.querySelectorAll(".scroller").addEventListener("mousewheel", scroll);
document.querySelectorAll(".scroller").addEventListener("DOMMouseScroll", scroll);
Colza answered 18/3, 2016 at 23:53 Comment(2)
Nice solution. Works great in chromeMoreover
I was having issue with firefox, this solution worked.Valerie
C
9

As variant, to avoid performance issues with scroll or mousewheel handling, you can use code like below:

css:

body.noscroll {
    overflow: hidden;
}
.scrollable {
    max-height: 200px;
    overflow-y: scroll;
    border: 1px solid #ccc;
}

html:

<div class="scrollable">
...A bunch of items to make the div scroll...
</div>
...A bunch of text to make the body scroll...

js:

var $document = $(document),
    $body = $('body'),
    $scrolable = $('.scrollable');

$scrolable.on({
          'mouseenter': function () {
            // add hack class to prevent workspace scroll when scroll outside
            $body.addClass('noscroll');
          },
          'mouseleave': function () {
            // remove hack class to allow scroll
            $body.removeClass('noscroll');
          }
        });

Example of work: http://jsbin.com/damuwinarata/4

Churchly answered 10/9, 2014 at 14:33 Comment(1)
The scrolls when is hide in some browsers move all the page to left because scroll disappear...Holomorphic
H
9

Angular JS Directive

I had to wrap an angular directive. The following is a Mashup of the other answers here. tested on Chrome and Internet Explorer 11.

var app = angular.module('myApp');

app.directive("preventParentScroll", function () {
    return {
        restrict: "A",
        scope: false,
        link: function (scope, elm, attr) {
            elm.bind('mousewheel', onMouseWheel);
            function onMouseWheel(e) {
                elm[0].scrollTop -= (e.wheelDeltaY || (e.originalEvent && (e.originalEvent.wheelDeltaY || e.originalEvent.wheelDelta)) || e.wheelDelta || 0);
                e.stopPropagation();
                e.preventDefault();
                e.returnValue = false;
            }
        }
    }
});

Usage

<div prevent-parent-scroll>
    ...
</div>

Hopes this helps the next person that gets here from a Google search.

Halmahera answered 26/7, 2016 at 9:9 Comment(3)
I really like the solution, it's working good. I just had to change one thing in angular 1.5. As e.originalEvet is not needed, just read wheel deltas directly from the e. (for both, wheelDeltaY and wheelDelta)Gravitative
@Gravitative thanks for your feedback. i think e.originalEvent is for supporting IEHalmahera
Hmm interesting, it sounds like IE. I will check it when I will be around win machine.Gravitative
I
7

Using native element scroll properties with the delta value from the mousewheel plugin:

$elem.on('mousewheel', function (e, delta) {
    // Restricts mouse scrolling to the scrolling range of this element.
    if (
        this.scrollTop < 1 && delta > 0 ||
        (this.clientHeight + this.scrollTop) === this.scrollHeight && delta < 0
    ) {
        e.preventDefault();
    }
});
Interfaith answered 19/6, 2013 at 12:47 Comment(0)
S
7

You can achieve this outcome with CSS, ie

.isolate-scrolling {
    overscroll-behavior: contain;
}

This will only scroll the parent container if your mouse leaves the child element to the parent.

Spin answered 27/10, 2021 at 2:35 Comment(0)
A
6

In case someone is still looking for a solution for this, the following plugin does the job http://mohammadyounes.github.io/jquery-scrollLock/

It fully addresses the issue of locking mouse wheel scroll inside a given container, preventing it from propagating to parent element.

It does not change wheel scrolling speed, user experience will not be affected. and you get the same behavior regardless of the OS mouse wheel vertical scrolling speed (On Windows it can be set to one screen or one line up to 100 lines per notch).

Demo: http://mohammadyounes.github.io/jquery-scrollLock/example/

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

Abeyant answered 24/10, 2014 at 12:47 Comment(2)
Sounds great, but when I try your demo in either Firefox or Chrome on MacOS, I still see the kind of problematic scrolling behavior described in this question.Dugong
@Dugong Check the General section in your System Preferences, if "Show Scroll Bars" is set to "When Scrolling", then you need to force it as the library won't be able to detect the presence of scroll-bars (see github.com/MohammadYounes/jquery-scrollLock/pull/3).Abeyant
B
4

amustill's answer as a knockout handler:

ko.bindingHandlers.preventParentScroll = {
    init: function (element, valueAccessor, allBindingsAccessor, context) {
        $(element).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();
                }
            }
        });
    }
};
Badillo answered 18/3, 2013 at 14:45 Comment(1)
I'd add a call to ko.utils.domNodeDisposal.addDisposeCallback in here too. I know it's probably not necessary since the node cleanup works between KO and jQuery works pretty well but it's nice to cover your bases :)Afrika
M
4

the method above is not that natural, after some googling I find a more nice solution , and no need of jQuery. see [1] and demo [2].

  var element = document.getElementById('uf-notice-ul');

  var isMacWebkit = (navigator.userAgent.indexOf("Macintosh") !== -1 &&
    navigator.userAgent.indexOf("WebKit") !== -1);
  var isFirefox = (navigator.userAgent.indexOf("firefox") !== -1);

  element.onwheel = wheelHandler; // Future browsers
  element.onmousewheel = wheelHandler; // Most current browsers
  if (isFirefox) {
    element.scrollTop = 0;
    element.addEventListener("DOMMouseScroll", wheelHandler, false);
  }
  // prevent from scrolling parrent elements
  function wheelHandler(event) {
    var e = event || window.event; // Standard or IE event object

    // Extract the amount of rotation from the event object, looking
    // for properties of a wheel event object, a mousewheel event object 
    // (in both its 2D and 1D forms), and the Firefox DOMMouseScroll event.
    // Scale the deltas so that one "click" toward the screen is 30 pixels.
    // If future browsers fire both "wheel" and "mousewheel" for the same
    // event, we'll end up double-counting it here. Hopefully, however,
    // cancelling the wheel event will prevent generation of mousewheel.
    var deltaX = e.deltaX * -30 || // wheel event
      e.wheelDeltaX / 4 || // mousewheel
      0; // property not defined
    var deltaY = e.deltaY * -30 || // wheel event
      e.wheelDeltaY / 4 || // mousewheel event in Webkit
      (e.wheelDeltaY === undefined && // if there is no 2D property then 
        e.wheelDelta / 4) || // use the 1D wheel property
      e.detail * -10 || // Firefox DOMMouseScroll event
      0; // property not defined

    // Most browsers generate one event with delta 120 per mousewheel click.
    // On Macs, however, the mousewheels seem to be velocity-sensitive and
    // the delta values are often larger multiples of 120, at 
    // least with the Apple Mouse. Use browser-testing to defeat this.
    if (isMacWebkit) {
      deltaX /= 30;
      deltaY /= 30;
    }
    e.currentTarget.scrollTop -= deltaY;
    // If we ever get a mousewheel or wheel event in (a future version of)
    // Firefox, then we don't need DOMMouseScroll anymore.
    if (isFirefox && e.type !== "DOMMouseScroll") {
      element.removeEventListener("DOMMouseScroll", wheelHandler, false);
    }
    // Don't let this event bubble. Prevent any default action.
    // This stops the browser from using the mousewheel event to scroll
    // the document. Hopefully calling preventDefault() on a wheel event
    // will also prevent the generation of a mousewheel event for the
    // same rotation.
    if (e.preventDefault) e.preventDefault();
    if (e.stopPropagation) e.stopPropagation();
    e.cancelBubble = true; // IE events
    e.returnValue = false; // IE events
    return false;
  }

[1] https://dimakuzmich.wordpress.com/2013/07/16/prevent-scrolling-of-parent-element-with-javascript/

[2] http://jsfiddle.net/dima_k/5mPkB/1/

Meshach answered 24/3, 2015 at 9:44 Comment(2)
Very nice solution - and this one blocks scrolling of the parent, even if the the small container doesn't need scrolling (it's content fits inside) - though some would bark at that - maybe make that feature optional with a second class...?Scathing
Ups, spoke too soon. In my Chrome on W10, scroll jumps straight to the bottom when wheeling down (on your jsfiddle) - that's no goodScathing
O
4

This actually works in AngularJS. Tested on Chrome and Firefox.

.directive('stopScroll', function () {
    return {
        restrict: 'A',
        link: function (scope, element, attr) {
            element.bind('mousewheel', function (e) {
                var $this = $(this),
                    scrollTop = this.scrollTop,
                    scrollHeight = this.scrollHeight,
                    height = $this.height(),
                    delta = (e.type == 'DOMMouseScroll' ?
                    e.originalEvent.detail * -40 :
                        e.originalEvent.wheelDelta),
                    up = delta > 0;

                var prevent = function() {
                    e.stopPropagation();
                    e.preventDefault();
                    e.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();
                }
            });
        }
    };
})
Odor answered 24/6, 2015 at 19:32 Comment(0)
S
4

We can simply use CSS. Give a style to the child scroll container element.

 style="overscroll-behavior: contain"

It doesn't trigger the parent's scroll event.

Sherrillsherrington answered 12/8, 2022 at 15:0 Comment(1)
interesting, but at this moment not supported yet by safari: caniuse.com/css-overscroll-behaviorDebag
M
2

my jQuery plugin:

$('.child').dontScrollParent();

$.fn.dontScrollParent = function()
{
    this.bind('mousewheel DOMMouseScroll',function(e)
    {
        var delta = e.originalEvent.wheelDelta || -e.originalEvent.detail;

        if (delta > 0 && $(this).scrollTop() <= 0)
            return false;
        if (delta < 0 && $(this).scrollTop() >= this.scrollHeight - $(this).height())
            return false;

        return true;
    });
}
Matthew answered 14/9, 2012 at 14:28 Comment(1)
This doesn't seem to work in Firefox.Plutocracy
S
2

I have a similar situation and here's how i solved it:
All my scrollable elements get the class scrollable.

$(document).on('wheel', '.scrollable', function(evt) {
  var offsetTop = this.scrollTop + parseInt(evt.originalEvent.deltaY, 10);
  var offsetBottom = this.scrollHeight - this.getBoundingClientRect().height - offsetTop;

  if (offsetTop < 0 || offsetBottom < 0) {
    evt.preventDefault();
  } else {
    evt.stopImmediatePropagation();
  }
});

stopImmediatePropagation() makes sure not to scroll parent scrollable area from scrollable child area.

Here's a vanilla JS implementation of it: http://jsbin.com/lugim/2/edit?js,output

Sello answered 21/8, 2014 at 15:6 Comment(4)
Can you please clarify what "offsetTop < 0 || offsetBottom < 0" means? What are you actually trying to assert?Shaffert
Consider using an intermediary variable to give it a name.Shaffert
... or rename the existing ones, or add some comments to explain.Shaffert
If I understand right, you are checking if the element will be scrolled to bottom or top boundary if the event should run it's course.Shaffert
P
2

New web dev here. This worked like a charm for me on both IE and Chrome.

static preventScrollPropagation(e: HTMLElement) {
    e.onmousewheel = (ev) => {
        var preventScroll = false;
        var isScrollingDown = ev.wheelDelta < 0;
        if (isScrollingDown) {
            var isAtBottom = e.scrollTop + e.clientHeight == e.scrollHeight;
            if (isAtBottom) {
                preventScroll = true;
            }
        } else {
            var isAtTop = e.scrollTop == 0;
            if (isAtTop) {
                preventScroll = true;
            }
        }
        if (preventScroll) {
            ev.preventDefault();
        }
    }
}

Don't let the number of lines fool you, it is quite simple - just a bit verbose for readability (self documenting code ftw right?)

Also I should mention that the language here is TypeScript, but as always, it is straightforward to convert it to JS.

Pastypat answered 19/3, 2015 at 21:18 Comment(0)
C
1

For those using MooTools, here is equivalent code:

            'mousewheel': function(event){
            var height = this.getSize().y;
            height -= 2;    // Not sure why I need this bodge
            if ((this.scrollTop === (this.scrollHeight - height) && event.wheel < 0) || 
                (this.scrollTop === 0 && event.wheel > 0)) {
                event.preventDefault();
            }

Bear in mind that I, like some others, had to tweak a value by a couple of px, that is what the height -= 2 is for.

Basically the main difference is that in MooTools, the delta info comes from event.wheel instead of an extra parameter passed to the event.

Also, I had problems if I bound this code to anything (event.target.scrollHeight for a bound function does not equal this.scrollHeight for a non-bound one)

Hope this helps someone as much as this post helped me ;)

Crosshead answered 11/12, 2011 at 12:14 Comment(1)
although i know nothing about your code, i'm willing to bet you had to subtract 2 from the height because getSize() doesn't account for the border (which in your case was 1px on both top and bottom).Gonsalve
H
1

Check out Leland Kwong's code.

Basic idea is to bind the wheeling event to the child element, and then use the native javascript property scrollHeight and the jquery property outerHeight of the child element to detect the end of the scroll, upon which return false to the wheeling event to prevent any scrolling.

var scrollableDist,curScrollPos,wheelEvent,dY;
$('#child-element').on('wheel', function(e){
  scrollableDist = $(this)[0].scrollHeight - $(this).outerHeight();
  curScrollPos = $(this).scrollTop();
  wheelEvent = e.originalEvent;
  dY = wheelEvent.deltaY;
  if ((dY>0 && curScrollPos >= scrollableDist) ||
      (dY<0 && curScrollPos <= 0)) {
    return false;
  }
});
Hawkes answered 28/11, 2015 at 13:44 Comment(1)
Nice, keeping it "local" - but what if deltaY is "chunky" - won't the last 20-30px remain hidden then ?Scathing
C
1

I yoinked this from the chosen library: https://github.com/harvesthq/chosen/blob/master/coffee/chosen.jquery.coffee

function preventParentScroll(evt) {
    var delta = evt.deltaY || -evt.wheelDelta || (evt && evt.detail)
    if (delta) {
        evt.preventDefault()
        if (evt.type ==  'DOMMouseScroll') {
            delta = delta * 40  
        }
        fakeTable.scrollTop = delta + fakeTable.scrollTop
    }
}
var el = document.getElementById('some-id')
el.addEventListener('mousewheel', preventParentScroll)
el.addEventListener('DOMMouseScroll', preventParentScroll)

This works for me.

Cruise answered 27/2, 2017 at 10:42 Comment(0)
S
0

I was searching for this for MooTools and this was the first that came up. The original MooTools example would work with scrolling up, but not scrolling down so I decided to write this one.


var stopScroll = function (e) {
    var scrollTo = null;
    if (e.event.type === 'mousewheel') {
        scrollTo = (e.event.wheelDelta * -1);
    } else if (e.event.type === 'DOMMouseScroll') {
        scrollTo = 40 * e.event.detail;
    }
    if (scrollTo) {
        e.preventDefault();
        this.scrollTo(0, scrollTo + this.scrollTop);
    }
    return false;
};

Usage:

(function)($){
    window.addEvent('domready', function(){
        $$('.scrollable').addEvents({
             'mousewheel': stopScroll,
             'DOMMouseScroll': stopScroll
        });
    });
})(document.id);
Simplify answered 1/4, 2013 at 21:4 Comment(0)
P
0

jQuery plugin with emulate natural scrolling for Internet Explorer

  $.fn.mousewheelStopPropagation = function(options) {
    options = $.extend({
        // defaults
        wheelstop: null // Function
        }, options);

    // Compatibilities
    var isMsIE = ('Microsoft Internet Explorer' === navigator.appName);
    var docElt = document.documentElement,
        mousewheelEventName = 'mousewheel';
    if('onmousewheel' in docElt) {
        mousewheelEventName = 'mousewheel';
    } else if('onwheel' in docElt) {
        mousewheelEventName = 'wheel';
    } else if('DOMMouseScroll' in docElt) {
        mousewheelEventName = 'DOMMouseScroll';
    }
    if(!mousewheelEventName) { return this; }

    function mousewheelPrevent(event) {
        event.preventDefault();
        event.stopPropagation();
        if('function' === typeof options.wheelstop) {
            options.wheelstop(event);
        }
    }

    return this.each(function() {
        var _this = this,
            $this = $(_this);
        $this.on(mousewheelEventName, function(event) {
            var origiEvent = event.originalEvent;
            var scrollTop = _this.scrollTop,
                scrollMax = _this.scrollHeight - $this.outerHeight(),
                delta = -origiEvent.wheelDelta;
            if(isNaN(delta)) {
                delta = origiEvent.deltaY;
            }
            var scrollUp = delta < 0;
            if((scrollUp && scrollTop <= 0) || (!scrollUp && scrollTop >= scrollMax)) {
                mousewheelPrevent(event);
            } else if(isMsIE) {
                // Fix Internet Explorer and emulate natural scrolling
                var animOpt = { duration:200, easing:'linear' };
                if(scrollUp && -delta > scrollTop) {
                    $this.stop(true).animate({ scrollTop:0 }, animOpt);
                    mousewheelPrevent(event);
                } else if(!scrollUp && delta > scrollMax - scrollTop) {
                    $this.stop(true).animate({ scrollTop:scrollMax }, animOpt);
                    mousewheelPrevent(event);
                }
            }
        });
    });
};

https://github.com/basselin/jquery-mousewheel-stop-propagation/blob/master/mousewheelStopPropagation.js

Polly answered 2/2, 2014 at 8:39 Comment(2)
While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes.Librate
Great job Tiben. Thanks for that, really. +1 for the effortLibrate
G
0

The best solution I could find was listening to the scroll event on the window and set the scrollTop to the previous scrollTop if the child div was visible.

prevScrollPos = 0
$(window).scroll (ev) ->
    if $('#mydiv').is(':visible')
        document.body.scrollTop = prevScrollPos
    else
        prevScrollPos = document.body.scrollTop

There is a flicker in the background of the child div if you fire a lot of scroll events, so this could be tweaked, but it is hardly noticed and it was sufficient for my use case.

Gnu answered 8/5, 2014 at 9:32 Comment(0)
L
0

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;:

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

Play with the demo here.

Lapierre answered 11/11, 2014 at 9:19 Comment(2)
I can't get this to work - there's a million lines of CSS and JS in your playgroundScathing
I recommend using overflow: hidden; on body as long as it doesn't scroll to the top of the page automatically. But when it does scroll to the top automatically, you should try using overflow: auto; height: 100% on body.Troubadour
T
0

There's also a funny trick to lock the parent's scrollTop when mouse hovers over a scrollable element. This way you don't have to implement your own wheel scrolling.

Here's an example for preventing document scroll, but it can be adjusted for any element.

scrollable.mouseenter(function ()
{
  var scroll = $(document).scrollTop();
  $(document).on('scroll.trap', function ()
  {
    if ($(document).scrollTop() != scroll) $(document).scrollTop(scroll);
  });
});

scrollable.mouseleave(function ()
{
  $(document).off('scroll.trap');
});
Toll answered 15/2, 2016 at 16:34 Comment(0)
S
0

M.K. offered a great plugin in his answer. Plugin can be found here. However, for the sake of completion, I thought it'd be a good idea to put it together in one answer for AngularJS.

  1. Start by injecting the bower or npm (whichever is preferred)

    bower install jquery-scrollLock --save
    npm install jquery-scroll-lock --save
    
  2. Add the following directive. I am choosing to add it as an attribute

    (function() {
       'use strict';
    
        angular
           .module('app')
           .directive('isolateScrolling', isolateScrolling);
    
           function isolateScrolling() {
               return {
                   restrict: 'A',
                   link: function(sc, elem, attrs) {
                      $('.scroll-container').scrollLock();
                   }
               }
           }
    })();
    
  3. And the important piece the plugin fails to document in their website is the HTML structure that it must follow.

    <div class="scroll-container locked">
        <div class="scrollable" isolate-scrolling>
             ... whatever ...
        </div>
    </div>
    

The attribute isolate-scrolling must contain the scrollable class and it all needs to be inside the scroll-container class or whatever class you choose and the locked class must be cascaded.

Sanches answered 16/1, 2017 at 19:46 Comment(0)
A
0

It is worth to mention that with modern frameworks like reactJS, AngularJS, VueJS, etc, there are easy solutions for this problem, when dealing with fixed position elements. Examples are side panels or overlaid elements.

The technique is called a "Portal", which means that one of the components used in the app, without the need to actually extract it from where you are using it, will mount its children at the bottom of the body element, outside of the parent you are trying to avoid scrolling.

Note that it will not avoid scrolling the body element itself. You can combine this technique and mounting your app in a scrolling div to achieve the expected result.

Example Portal implementation in React's material-ui: https://material-ui-next.com/api/portal/

Achaean answered 14/5, 2018 at 10:14 Comment(0)
G
0

There is ES 6 crossbrowser + mobile vanila js decision:

function stopParentScroll(selector) {
    let last_touch;
    let MouseWheelHandler = (e, selector) => {
        let delta;
        if(e.deltaY)
            delta = e.deltaY;
        else if(e.wheelDelta)
            delta = e.wheelDelta;
        else if(e.changedTouches){
            if(!last_touch){
                last_touch = e.changedTouches[0].clientY;
            }
            else{
                if(e.changedTouches[0].clientY > last_touch){
                    delta = -1;
                }
                else{
                    delta = 1;
                }
            }
        }
        let prevent = function() {
            e.stopPropagation();
            e.preventDefault();
            e.returnValue = false;
            return false;
        };

        if(selector.scrollTop === 0 && delta < 0){
            return prevent();
        }
        else if(selector.scrollTop === (selector.scrollHeight - selector.clientHeight) && delta > 0){
            return prevent();
        }
    };

    selector.onwheel = e => {MouseWheelHandler(e, selector)}; 
    selector.onmousewheel = e => {MouseWheelHandler(e, selector)}; 
    selector.ontouchmove  = e => {MouseWheelHandler(e, selector)};
}
Goosy answered 14/12, 2018 at 2:0 Comment(0)
B
-1

Simple solution with mouseweel event:

$('.element').bind('mousewheel', function(e, d) {
    console.log(this.scrollTop,this.scrollHeight,this.offsetHeight,d);
    if((this.scrollTop === (this.scrollHeight - this.offsetHeight) && d < 0)
        || (this.scrollTop === 0 && d > 0)) {
        e.preventDefault();
    }
});
Berryberryhill answered 5/11, 2015 at 14:4 Comment(0)
I
-2

You can try it this way:

$('#element').on('shown', function(){ 
   $('body').css('overflow-y', 'hidden');
   $('body').css('margin-left', '-17px');
});

$('#element').on('hide', function(){ 
   $('body').css('overflow-y', 'scroll');
   $('body').css('margin-left', '0px');
});
Isaacson answered 23/7, 2012 at 13:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.