useIntersectionObserver
December 21, 2025
The Intersection Observer API is perfect for lazy loading images, infinite scrolling, animations, and tracking visibility. This hook wraps the API in a React-friendly way.
import { useEffect, useRef, useState, RefObject } from 'react';
interface UseIntersectionObserverOptions extends IntersectionObserverInit {
triggerOnce?: boolean;
}
interface UseIntersectionObserverReturn {
ref: RefObject<HTMLElement>;
isIntersecting: boolean;
entry: IntersectionObserverEntry | null;
}
export function useIntersectionObserver(
options: UseIntersectionObserverOptions = {}
): UseIntersectionObserverReturn {
const { triggerOnce = false, ...observerOptions } = options;
const [isIntersecting, setIsIntersecting] = useState(false);
const [entry, setEntry] = useState<IntersectionObserverEntry | null>(null);
const elementRef = useRef<HTMLElement>(null);
useEffect(() => {
const element = elementRef.current;
if (!element) return;
const observer = new IntersectionObserver(([entry]) => {
setIsIntersecting(entry.isIntersecting);
setEntry(entry);
if (triggerOnce && entry.isIntersecting) {
observer.disconnect();
}
}, observerOptions);
observer.observe(element);
return () => {
observer.disconnect();
};
}, [triggerOnce, observerOptions.root, observerOptions.rootMargin, observerOptions.threshold]);
return { ref: elementRef, isIntersecting, entry };
}
How It Works
- Creates an IntersectionObserver that watches the referenced element.
- Updates state when the element enters or leaves the viewport.
- Supports
triggerOnceoption to stop observing after first intersection. - Cleans up the observer on unmount.
Example Usage
function LazyImage({ src, alt }: { src: string; alt: string }) {
const { ref, isIntersecting } = useIntersectionObserver({
triggerOnce: true,
});
return (
<div ref={ref}>
{isIntersecting ? (
<img src={src} alt={alt} />
) : (
<div className="placeholder">Loading...</div>
)}
</div>
);
}
Infinite Scroll Example
function InfiniteScroll({ onLoadMore }: { onLoadMore: () => void }) {
const { ref, isIntersecting } = useIntersectionObserver({
rootMargin: '100px',
});
useEffect(() => {
if (isIntersecting) {
onLoadMore();
}
}, [isIntersecting, onLoadMore]);
return <div ref={ref}>Loading more...</div>;
}
Animation on Scroll
function AnimatedSection({ children }: { children: React.ReactNode }) {
const { ref, isIntersecting } = useIntersectionObserver({
threshold: 0.1,
});
return (
<div
ref={ref}
className={`transition-opacity duration-500 ${
isIntersecting ? 'opacity-100' : 'opacity-0'
}`}
>
{children}
</div>
);
}
Use Cases
- Lazy loading images
- Infinite scrolling
- Scroll-triggered animations
- Analytics tracking (view impressions)
- Sticky headers/navigation
- Progress indicators
Notes
- The observer is automatically cleaned up when the component unmounts.
triggerOnceis useful for one-time animations or lazy loading.- Adjust
thresholdto control when the callback fires (0.0 to 1.0). rootMarginallows you to trigger before the element is fully visible.
A powerful hook that brings the Intersection Observer API into React, enabling performant scroll-based features.