useScrollPosition

Layout/Viewport

A React hook that tracks window scroll position in real-time, perfect for scroll-based animations and progress indicators.

Demo

Scroll this page to see the coordinates update in real-time:

Scroll X
0px
Scroll Y
0px
Scroll Progress
0% of page scrolled
Horizontal Scroll Demo
Scroll horizontally to see the X coordinate change
Scroll Section 1
This section helps demonstrate vertical scrolling. Keep scrolling to see more!
Scroll Section 2
This section helps demonstrate vertical scrolling. Keep scrolling to see more!
Scroll Section 3
This section helps demonstrate vertical scrolling. Keep scrolling to see more!

Installation

npm install @thibault.sh/hooks

Usage

import { useScrollPosition } from '@thibault.sh/hooks/useScrollPosition';

function ScrollProgress() {
  const { y } = useScrollPosition();
  const progress = Math.min(
    (y / (document.documentElement.scrollHeight - window.innerHeight)) * 100,
    100
  );

  return (
    <div className="fixed top-0 left-0 w-full h-1 bg-gray-200">
      <div
        className="h-full bg-blue-500 transition-all"
        style={{ width: `${progress}%` }}
      />
    </div>
  );
}

API

Returns

Object containing current scroll x and y coordinates

Features

  • Real-time Updates

    Automatically updates when window scroll position changes

  • Performance Optimized

    Uses throttled scroll event listener to prevent excessive re-renders

  • Bi-directional Tracking

    Tracks both horizontal (x) and vertical (y) scroll positions

  • SSR Compatible

    Safely handles server-side rendering with default coordinates

Scroll-based Animation Example

import { useScrollPosition } from '@thibault.sh/hooks/useScrollPosition';

function ScrollAnimation() {
  const { y } = useScrollPosition();
  const elementRef = useRef<HTMLDivElement>(null);

  // Calculate element's visibility based on scroll position
  const calculateVisibility = () => {
    if (!elementRef.current) return 0;
    const rect = elementRef.current.getBoundingClientRect();
    const windowHeight = window.innerHeight;
    
    if (rect.top >= windowHeight || rect.bottom <= 0) return 0;
    if (rect.top <= 0 && rect.bottom >= windowHeight) return 1;
    
    const visibleHeight = Math.min(rect.bottom, windowHeight) - Math.max(rect.top, 0);
    return visibleHeight / rect.height;
  };

  const visibility = calculateVisibility();

  return (
    <div
      ref={elementRef}
      style={{
        opacity: visibility,
        transform: `translateY(${(1 - visibility) * 50}px)`,
        transition: 'transform 0.2s ease-out'
      }}
    >
      {/* Content */}
    </div>
  );
}

Infinite Scroll Example

import { useScrollPosition } from '@thibault.sh/hooks/useScrollPosition';
import { useEffect } from 'react';

function InfiniteScroll({ onLoadMore }: { onLoadMore: () => void }) {
  const { y } = useScrollPosition();
  
  useEffect(() => {
    const scrollHeight = document.documentElement.scrollHeight;
    const clientHeight = document.documentElement.clientHeight;
    const remainingScroll = scrollHeight - clientHeight - y;
    
    // Load more when user is near bottom
    if (remainingScroll < 200) {
      onLoadMore();
    }
  }, [y, onLoadMore]);

  return (
    <div className="space-y-4">
      {/* Content */}
      <div className="h-8 flex items-center justify-center">
        {y > 0 && remainingScroll < 200 && (
          <span className="text-sm text-muted-foreground">Loading more...</span>
        )}
      </div>
    </div>
  );
}

Scroll-to-Top Button Example

import { useScrollPosition } from '@thibault.sh/hooks/useScrollPosition';

function ScrollToTopButton() {
  const { y } = useScrollPosition();
  
  const scrollToTop = () => {
    window.scrollTo({
      top: 0,
      behavior: 'smooth'
    });
  };

  if (y < 200) return null;

  return (
    <button
      onClick={scrollToTop}
      className="fixed bottom-4 right-4 p-2 bg-orange-500 text-white rounded-full
                opacity-0 transition-opacity duration-200 hover:bg-orange-600
                data-[visible=true]:opacity-100"
      data-visible={y >= 200}
    >
      <svg
        className="w-6 h-6"
        fill="none"
        stroke="currentColor"
        viewBox="0 0 24 24"
      >
        <path
          strokeLinecap="round"
          strokeLinejoin="round"
          strokeWidth={2}
          d="M5 10l7-7m0 0l7 7m-7-7v18"
        />
      </svg>
    </button>
  );
}