Snippets/react/useIntersectionObserver Hook
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/UX
reacthooksanimationperformance

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 freezeOnceVisible flag 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.