Adding a shadow to the scroll area to indicate overflow area.

function updateShadows() {
  const scrollLeft = content.scrollLeft;
  const scrollWidth = content.scrollWidth;
  const clientWidth = content.clientWidth;

  leftShadow.style.opacity = scrollLeft > 0 ? "1" : "0";
  rightShadow.style.opacity =
    scrollLeft + clientWidth < scrollWidth - 1 ? "1" : "0";
}

Result

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

This can be also used in vertical scroll by using scrollTop instead of scrollLeft.

A complete util vanilla javascript function is shown below.

function createScrollShadow(wrapperEl) {
  if (!wrapperEl) return;

  const contentEl = wrapperEl.querySelector(".scroll-content");
  if (!contentEl) {
    console.warn("Missing .scroll-content inside wrapper");
    return;
  }

  const leftShadow = document.createElement("div");
  const rightShadow = document.createElement("div");

  leftShadow.classList.add("scroll-shadow", "left");
  rightShadow.classList.add("scroll-shadow", "right");

  wrapperEl.appendChild(leftShadow);
  wrapperEl.appendChild(rightShadow);

  function updateShadows() {
    const scrollLeft = contentEl.scrollLeft;
    const scrollWidth = contentEl.scrollWidth;
    const clientWidth = contentEl.clientWidth;

    leftShadow.style.opacity = scrollLeft > 0 ? "1" : "0";
    rightShadow.style.opacity =
      scrollLeft + clientWidth < scrollWidth - 1 ? "1" : "0";
  }

  contentEl.addEventListener("scroll", updateShadows);
  window.addEventListener("resize", updateShadows);

  updateShadows(); // initial check

  return {
    destroy() {
      contentEl.removeEventListener("scroll", updateShadows);
      window.removeEventListener("resize", updateShadows);
      leftShadow.remove();
      rightShadow.remove();
    },
    update: updateShadows,
  };
}

or this headless react hook version which gives you state which scrollable direction is available.

import { useState, useEffect, useCallback, RefObject } from "react";

type ScrollState = {
  isScrollableTop: boolean;
  isScrollableBottom: boolean;
  isScrollableLeft: boolean;
  isScrollableRight: boolean;
};

/**
 * A hook to monitor the scrollable state of an HTML element,
 * provides states to know which direct (top/bottom) are scrollable
 */
export const useScrollableArea = (
  element: RefObject<HTMLDivElement>,
): ScrollState => {
  const [scrollState, setScrollState] = useState<ScrollState>({
    isScrollableTop: false,
    isScrollableBottom: false,
    isScrollableLeft: false,
    isScrollableRight: false,
  });

  const checkScrollableArea = useCallback(() => {
    if (!element.current) return;

    const {
      scrollTop,
      scrollHeight,
      clientHeight,
      scrollLeft,
      scrollWidth,
      clientWidth,
    } = element.current;

    const isScrollableTop = scrollTop > 0;
    const isScrollableBottom = scrollTop + clientHeight < scrollHeight;

    const isScrollableLeft = scrollLeft > 0;
    const isScrollableRight = scrollLeft + clientWidth < scrollWidth;

    setScrollState({
      isScrollableTop,
      isScrollableBottom,
      isScrollableLeft,
      isScrollableRight,
    });
  }, [element]);

  useEffect(() => {
    if (!element.current) return;

    // Initial check
    checkScrollableArea();

    // Attach scroll event listener
    element.current.addEventListener("scroll", checkScrollableArea);

    // Cleanup
    return () => {
      element.current?.removeEventListener("scroll", checkScrollableArea);
    };
  }, [element, checkScrollableArea]);

  return scrollState;
};

A shadow helps indicate that the area is scrollable.