VUE

useScrollPosition Composable

Reactive scroll position tracking with debounce and direction detection

Vue3ComposablesScrollPositionReactive

Code

import { ref, onMounted, onUnmounted } from 'vue';

interface ScrollPosition {
  x: Ref<number>;
  y: Ref<number>;
  directionX: Ref<'left' | 'right' | 'none'>;
  directionY: Ref<'up' | 'down' | 'none'>;
  isTop: Ref<boolean>;
  isBottom: Ref<boolean>;
}

export function useScrollPosition(
  target: HTMLElement | Window = window,
  debounceDelay = 16
): ScrollPosition {
  const x = ref(0);
  const y = ref(0);
  const directionX = ref<'left' | 'right' | 'none'>('none');
  const directionY = ref<'up' | 'down' | 'none'>('none');
  const isTop = ref(true);
  const isBottom = ref(false);
  let lastX = 0;
  let lastY = 0;
  let scrollTimeout: ReturnType<typeof setTimeout> | null = null;

  const updateScrollPosition = () => {
    const newX = target === window ? window.scrollX : (target as HTMLElement).scrollLeft;
    const newY = target === window ? window.scrollY : (target as HTMLElement).scrollTop;

    // Update direction
    directionX.value = newX > lastX ? 'right' : newX < lastX ? 'left' : 'none';
    directionY.value = newY > lastY ? 'down' : newY < lastY ? 'up' : 'none';

    // Update position
    x.value = newX;
    y.value = newY;

    // Update top/bottom state
    isTop.value = newY === 0;
    __TOKEN_55__ (target === window) {
      isBottom.value = newY + window.innerHeight >= document.documentElement.scrollHeight;
    } else {
      const el = target as HTMLElement;
      isBottom.value = newY + el.clientHeight >= el.scrollHeight;
    }

    // Update last positions
    lastX = newX;
    lastY = newY;
  };

  const handleScroll = () => {
    __TOKEN_59__ (scrollTimeout) clearTimeout(scrollTimeout);
    scrollTimeout = setTimeout(updateScrollPosition, debounceDelay);
  };

  onMounted(() => {
    target.addEventListener('scroll', handleScroll, { passive: true });
    updateScrollPosition(); // Initial position
  });

  onUnmounted(() => {
    target.removeEventListener('scroll', handleScroll);
    __TOKEN_60__ (scrollTimeout) clearTimeout(scrollTimeout);
  });

  return { x, y, directionX, directionY, isTop, isBottom };
}

// Usage example
// const { y, directionY, isTop } = useScrollPosition();
// watch(directionY, (dir) => {
//   if (dir === 'down' && !isTop.value) {
//     headerRef.value.classList.add('sticky');
//   } else {
//     headerRef.value.classList.remove('sticky');
//   }
// });