PRO
PRO

Reputation: 394

Determine if an element is scrollable

I'm looking for a way to determine if an element is scrollable in given direction. That is, if I can call Element.scrollBy to scroll it.

I've searched around extensively and ended up with:

/**
 * Whether the element can be scrolled.
 * @param {HTMLElement} el The element.
 * @param {boolean} vertical Whether the scroll is vertical.
 * @param {boolean} plus Whether the scroll is positive (down or right).
 * @returns {boolean} Whether the element can be scrolled.
 */
function canScroll(el, vertical = true, plus = true) {
    const style = window.getComputedStyle(el);
    const overflow = vertical ? style.overflowY : style.overflowX;
    const scrollSize = vertical ? el.scrollHeight : el.scrollWidth;
    const clientSize = vertical ? el.clientHeight : el.clientWidth;
    const scrollPos = vertical ? el.scrollTop : el.scrollLeft;
    const isScrollable = scrollSize > clientSize;
    const canScrollFurther = plus
        ? scrollPos + clientSize < scrollSize
        : scrollPos > 0;
    return (
        isScrollable &&
        canScrollFurther &&
        !overflow.includes("visible") &&
        !overflow.includes("hidden")
    );
}

The snippet works quite well on most occasions, but unfortunately not all occasions. Here is an example on CodePen where it is called on document.body, and document.body.clientHeight !== document.body.scrollHeight. In this case, it returned true, while it should return false, since calling document.body.scrollBy({top: 100}) doesn't yield any result.

How can I improve this canScroll function, so that it can correctly handle the given example?

Upvotes: 0

Views: 124

Answers (2)

Aayla Secura
Aayla Secura

Reputation: 475

EDIT: Original version was failing with scroll-behavior: smooth via CSS, fixed.


As you discovered, checking if clientHeight is less than scrollHeight is not always reliable. I think the best way is to first check if the current scroll position is not 0, if it is, the element is scrollable. Otherwise you can try scrolling by 1 px, check offset again and revert to 0. This should not cause any visible movement as it all happens in a single frame.

Here is a working example.

// axis is one of "x" or "y", or if omitted, will check both
const isScrollable = (element, axis) => {
  if (!axis) {
    return isScrollable(element, "x") || isScrollable(element, "y");
  }

  const offset = axis === "x" ? "Left" : "Top";

  // Checking if clientHeight < scrollHeight is not always reliable
  if (element[`scroll${offset}`]) {
    return true;
  }

  // element[`scroll${offset}`] = 1; // won't work with smooth scroll-behavior
  element.scrollTo({[offset.toLowerCase()]: 1, behavior: "instant"});
  const canScroll = element[`scroll${offset}`] > 0;
  // element[`scroll${offset}`] = 0;
  element.scrollTo({[offset.toLowerCase()]: 0, behavior: "instant"});
  return canScroll;
};

Upvotes: 1

PRO
PRO

Reputation: 394

I've come up with a rather hacky solution. The main idea is to try to scroll, and detect whether the scroll has been successful:

/**
 * Detect whether the element can be scrolled using a hacky detection method.
 * @param {HTMLElement} el The element.
 * @param {boolean} vertical Whether the scroll is vertical.
 * @param {boolean} plus Whether the scroll is positive (down or right).
 * @returns {boolean} Whether the element can be scrolled.
 */
function hackyDetect(el, vertical = true, plus = true) {
    const attrs = vertical ? ["top", "scrollTop"] : ["left", "scrollLeft"];
    const delta = plus ? 1 : -1;
    const before = el[attrs[1]]; // Determine `scrollTop`/`scrollLeft` before trying to scroll
    el.scrollBy({ [attrs[0]]: delta, behavior: "instant" }); // Try to scroll in the specified direction
    const after = el[attrs[1]]; // Determine `scrollTop`/`scrollLeft` after we've scrolled
    if (before === after) return false;
    else {
        el.scrollBy({ [attrs[0]]: -delta, behavior: "instant" }); // Scroll back if applicable
        return true;
    }
}

Full demo available at the original CodePen. The drawback is that it'll interrupt ongoing scroll on el if it is scrollable, and might be less efficient, so I think it might be better to combine both methods: canScroll(...) && hackyDetect(...).

Upvotes: 1

Related Questions