intermediatereact
useIntersectionObserver Hook
A performant React hook to track when an element enters or leaves the viewport. Perfect for scroll animations and lazy loading.
Published December 2, 2025
Updated December 2, 2025
UI/UXreacthooksanimationperformance
useIntersectionObserver Hook
This hook wraps the native IntersectionObserver API to provide a declarative way to track element visibility. It's essential for implementing "fade-in on scroll" effects, lazy loading images, or infinite scrolling lists.
Highlights
- Performance First: Uses a single observer instance (per unique options) or cleans up efficiently.
- Freeze Once Visible: Optional
freezeOnceVisibleflag to stop observing after the element enters the viewport (great for one-off animations). - Type Safe: Fully typed with TypeScript.
Code
import { useEffect, useRef, useState } from 'react';
interface UseIntersectionObserverArgs extends IntersectionObserverInit {
freezeOnceVisible?: boolean;
}
export function useIntersectionObserver(
elementRef: React.RefObject<Element | null>,
{
threshold = 0,
root = null,
rootMargin = '0%',
freezeOnceVisible = false,
}: UseIntersectionObserverArgs = {}
): IntersectionObserverEntry | undefined {
const [entry, setEntry] = useState<IntersectionObserverEntry>();
const frozen = entry?.isIntersecting && freezeOnceVisible;
useEffect(() => {
const node = elementRef?.current;
const hasIOSupport = !!window.IntersectionObserver;
if (!hasIOSupport || frozen || !node) return;
const observerParams = { threshold, root, rootMargin };
const observer = new IntersectionObserver(([newEntry]) => {
setEntry(newEntry);
}, observerParams);
observer.observe(node);
return () => observer.disconnect();
}, [elementRef, JSON.stringify(threshold), root, rootMargin, frozen]);
return entry;
}Usage
Here is how you can use it to create a "Fade In" animation component:
import { useRef } from 'react';
import { useIntersectionObserver } from '@/hooks/useIntersectionObserver';
export function FadeInSection({ children }: { children: React.ReactNode }) {
const ref = useRef<HTMLDivElement>(null);
const entry = useIntersectionObserver(ref, {
freezeOnceVisible: true, // Only fade in once
threshold: 0.1, // Trigger when 10% visible
});
const isVisible = !!entry?.isIntersecting;
return (
<div
ref={ref}
className={`transition-opacity duration-1000 ${
isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-10'
}`}
>
{children}
</div>
);
}💡 Pro Tip: For complex lists with hundreds of items, consider using a single parent observer instead of creating a hook for every item to save memory.