Prevent scrolling of parent element when inner element scroll position reaches top/bottom? [duplicate]

Prevent scrolling of parent element when inner element scroll position reaches top/bottom? [duplicate]

This question already has an answer here:

prevent Scroll bubbling from element to window

14 answers

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

Solutions/Answers:

Solution 1:

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

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

Solution 2:

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:

Related:  Create a span element inside another element using javascript

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.

Solution 3:

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

Solution 4:

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.

Solution 5:

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

Related:  Thickness of lines using THREE.LineBasicMaterial

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.

Solution 6:

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:

Related:  Remove all classes except one

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