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