95

I have this situation in which I need to scroll an element into the viewport. The problem is that I don't know which element is scrollable. For example, in Portrait the body is scrollable and in Landscape its an other element (and there are more situation which change the scrollable element)

Now the question, given an element which needs to be scrolled into the viewport, what is the best way to find its first scrollable parent ?

I've setup a demo here. With the button you can toggle between two different situations

<div class="outer">
    <div class="inner">
        <div class="content"> 
            ...
            <span>Scroll me into view</span>
        </div>
    </div>
</div>

The body is scrollable or .outer

Any suggestions ?

Jeanluca Scaljeri
  • 26,343
  • 56
  • 205
  • 333

10 Answers10

91

Just check if the scrollbar is visible, if not look to the parent.

function getScrollParent(node) {
  if (node == null) {
    return null;
  }

  if (node.scrollHeight > node.clientHeight) {
    return node;
  } else {
    return getScrollParent(node.parentNode);
  }
}
diedu
  • 19,277
  • 4
  • 32
  • 49
Stefano Nardo
  • 1,567
  • 2
  • 13
  • 23
  • 2
    If the node has an `inline` parent, `clientHeight` will be 0 and it will be returned here. Better to ignore nodes with `clientHeight` of 0. – Adam Leggett Mar 28 '17 at 22:24
  • 19
    This is nice but doesn't work for nodes that are overflowing but still cannot scroll. If you add a check for the [overflowY value](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#Determine_if_an_element_has_been_totally_scrolled), it seems to work well. `let overflowY = window.getComputedStyle(node).overflowY;` `let isScrollable = overflowY !== 'visible' && overflowY !== 'hidden';` `if (isScrollable && node.scrollHeight > node.clientHeight) {` – RustyToms Apr 19 '17 at 20:59
  • 9
    According @RustyToms and OP, I rewrite this one: https://gist.github.com/twxia/bb20843c495a49644be6ea3804c0d775 – twxia Jan 05 '18 at 03:53
  • 2
    aka `while (node && node.scrollHeight <= node.clientHeight) node = node.parentNode;` – Clément May 16 '20 at 04:29
61

This is a pure JS port of the jQuery UI scrollParent method that cweston spoke of. I went with this rather than the accepted answer's solution which will not find the scroll parent if there's no content overflow yet.

The one difference with my port is that, if no parent is found with the right value for the CSS overflow property, I return the <body> element. JQuery UI, instead returned the document object. This is odd as values like .scrollTop can be retrieved from the <body> but not the document.

function getScrollParent(element, includeHidden) {
    var style = getComputedStyle(element);
    var excludeStaticParent = style.position === "absolute";
    var overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/;

    if (style.position === "fixed") return document.body;
    for (var parent = element; (parent = parent.parentElement);) {
        style = getComputedStyle(parent);
        if (excludeStaticParent && style.position === "static") {
            continue;
        }
        if (overflowRegex.test(style.overflow + style.overflowY + style.overflowX)) return parent;
    }

    return document.body;
}
Community
  • 1
  • 1
Web_Designer
  • 72,308
  • 93
  • 206
  • 262
  • Maybe I'm missing something, but it's backwards for me.... `.scrollTop` can be read from the document: `document.documentElement.scrollTop` but the `document.body.scrollTop` returns `0` regardless of the actual scroll position (you can test in the devtools right here on SO). That would mean that it makes sense that Jquery UI returns the document instead of the body. – Skeets Apr 17 '19 at 08:19
  • Same with jQuery: `$("body").scrollTop()` always returns `0`, `$(document).scrollTop()` returns your actual scroll position. – Skeets Apr 17 '19 at 08:20
  • 6
    Probably better to return `document.scrollingElement` to address @Skeets point. Maybe this still varies between browsers? – natevw Apr 17 '19 at 23:53
  • @Skeets and the answer author: Have you agreed over the document standards mode? Maybe such scrolling html/body element things depend on standard/quirks mode. Just an idea, I think I had such issues before. – ygoe Jun 22 '22 at 19:27
  • What's the point of doing `style.overflow + style.overflowY + style.overflowX`? Isn't `style.overflow` always composed of the other two? See https://jsbin.com/zumeniwasa/edit?html,css,js,console – thorn0 Jul 04 '23 at 17:47
13

the answer with most votes doesn't work in all cases scrollHeight > clientHeight can be true even if there is no scrollbar.

I found this gist solution https://github.com/olahol/scrollparent.js/blob/master/scrollparent.js#L13

^ total credit to https://github.com/olahol who wrote the code.

Refactored it to es6:

export const getScrollParent = (node) => {
  const regex = /(auto|scroll)/;
  const parents = (_node, ps) => {
    if (_node.parentNode === null) { return ps; }
    return parents(_node.parentNode, ps.concat([_node]));
  };

  const style = (_node, prop) => getComputedStyle(_node, null).getPropertyValue(prop);
  const overflow = _node => style(_node, 'overflow') + style(_node, 'overflow-y') + style(_node, 'overflow-x');
  const scroll = _node => regex.test(overflow(_node));

  /* eslint-disable consistent-return */
  const scrollParent = (_node) => {
    if (!(_node instanceof HTMLElement || _node instanceof SVGElement)) {
      return;
    }

    const ps = parents(_node.parentNode, []);

    for (let i = 0; i < ps.length; i += 1) {
      if (scroll(ps[i])) {
        return ps[i];
      }
    }

    return document.scrollingElement || document.documentElement;
  };

  return scrollParent(node);
  /* eslint-enable consistent-return */
};

you can use it like:

const $yourElement = document.querySelector('.your-class-or-selector');
getScrollParent($yourElement);
ncubica
  • 8,169
  • 9
  • 54
  • 72
  • You should link to the answer you're referencing directly instead of saying "the answer with most votes", since votes change over time. – Nathan Arthur May 29 '21 at 00:14
  • This function is very wasteful and this can get VERY expensive depending on how many elements you have. It builds up an array with all parents upfront, even if your scroll is 1 node up. It calls getComputedStyle for every property which is also expensive. Not to mention unnecessary regex tests and function calls. – Vitim.us Apr 28 '22 at 03:11
  • ^ the beauty of SO is that you can propose an alternative solution even more post an answer so everybody can learn from you, go and do it, show us a better solution ;) – ncubica Apr 28 '22 at 03:52
9

If you are using jQuery UI you can use the scrollParent method. Have a look at the API or the source.

From the API:

.scrollParent(): Get the closest ancestor element that is scrollable

This method does not accept any arguments. This method finds the nearest ancestor that allows scrolling. In other words, the .scrollParent() method finds the element that the currently selected element will scroll within.

Note: This method only works on jQuery objects containing one element.

If you are not using jQuery UI but are using jQuery, then there are alternative independent libraries providing similar functionality, such as:

jquery-scrollparent

Web_Designer
  • 72,308
  • 93
  • 206
  • 262
cweston
  • 11,297
  • 19
  • 82
  • 107
  • 2
    I ported this to vanilla JS here: http://stackoverflow.com/questions/35939886/find-first-scrollable-parent#42543908 – Web_Designer Mar 01 '17 at 23:13
3

Using google chrome dev tools, when you've scrolled partially down the page, inspect the page, select the DOM node that you think might be the one that is being scrolled. Then pull up the console (hit ESC from within the Elements tab of the dev tools) and type $0.scrollTop. This will print out the current scroll position of that element. If it is NOT 0 then you will know that that is the element that is being scrolled.

Drew2
  • 391
  • 1
  • 13
  • 3
    Extending your comment, the following code snipped will query all elements that has scrolled. `Array.prototype.slice.call(document.querySelectorAll('*')).filter(el => el.scrollTop > 0)` – Emric Månsson May 05 '20 at 06:45
1

I think you want this.

$('button').click(function() {
  $("body").addClass("body");
  $('.outer').toggleClass('scroller');
  check($(".content"));
});

function check(el) {
  var overflowY = el.css("overflow-y");  
  if (overflowY == "scroll") {
    alert(el.attr("class") + " has");
  } else {
    if(el.parent().length > 0)
      check(el.parent());
    else 
      return false;
  }
}
body {
  height: 450px;
  overflow-y: scroll;
}

div.inner {
  width: 200px;
  height: 400px;
  border: 1px solid #000;
}

div.outer {
  width: 200px;
  height: 200px;
}

div.outer.scroller {
  overflow-y: scroll;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<button>
  toggle
</button>
<div class="outer">
  <div class="inner">
    <div class="content">
      "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor
      in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." adipiscing elit, sed do eiusmod tempor incididunt ut
      labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur
      sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
    </div>
  </div>
</div>
NiZa
  • 3,806
  • 1
  • 21
  • 29
1

Building upon further on the @Web_Designer's answer,

If you are passing the jQuery object for that element and are getting the following error,

Failed to execute 'getComputedStyle' on 'Window': parameter 1 is not of type 'Element'

Then try passing just the Dom Node element which btw resides at array key 0 if the element is a single element. Eg.

getScrollParent(jQuery("#" + formid)[0])
Mohd Abdul Mujib
  • 13,071
  • 8
  • 64
  • 88
1

this is what I have so-far that seems to work across web component custom element shadowRoots; we might expand it further to add a scroll event handler once scrolling starts to unload this handling and track a specific node's scroll event

function scroller(node){ return node.scrollTop }
function handler(event){
        const path = event.composedPath();
        const scrollNode = path.find(scroller);
        const scrollNodes = path.filter(scroller);
        console.warn('scroll!',{scrollNode, scrollNodes});
}
window.addEventListener('mousewheel', handler, {capture: true});
window.addEventListener('keydown', handler, {capture: true});
jimmont
  • 2,304
  • 1
  • 27
  • 29
1

A more efficient version of @ncubica's answer.

In this version, we check each parent one by one, and stop at the first one which is scrollable. Also, getComputedStyle() only gets called once per parent.

const isScrollable = (node: Element) => {
  if (!(node instanceof HTMLElement || node instanceof SVGElement)) {
    return false
  }
  const style = getComputedStyle(node)
  return ['overflow', 'overflow-x', 'overflow-y'].some((propertyName) => {
    const value = style.getPropertyValue(propertyName)
    return value === 'auto' || value === 'scroll'
  })
}

export const getScrollParent = (node: Element): Element => {
  let currentParent = node.parentElement
  while (currentParent) {
    if (isScrollable(currentParent)) {
      return currentParent
    }
    currentParent = currentParent.parentElement
  }
  return document.scrollingElement || document.documentElement
}
Gabriel Jablonski
  • 854
  • 1
  • 6
  • 17
-2

This is the way to find what you want.

document.addEventListener('scroll', (e) => {
    console.log(e.target.scrollingElement.scrollTop);
})
zheng li
  • 501
  • 1
  • 4
  • 10