Rohan Shewale's Blog

Scroll area shadow

February 23, 2025 | 6 Minute Read

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.