skip to Main Content

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?

2

Answers


  1. Chosen as BEST ANSWER

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

    The drawback is that it'll interrupt ongoing scroll on el if it is scrollable, and might be less efficient.


  2. 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;
      const canScroll = element[`scroll${offset}`] > 0;
      element[`scroll${offset}`] = 0;
      return canScroll;
    };
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search