PatternHookScrollVisibility

Intersection Observer Hook

A hook for detecting when elements enter or leave the viewport. Useful for lazy loading, infinite scroll, and scroll-triggered animations.

Preview

Box 1: HiddenBox 2: HiddenBox 3: Hidden
Box 1
Box 2
Box 3

Scroll the container to see visibility change

Code

import { useState, useEffect, useRef, RefObject } from 'react'

interface IntersectionOptions {
  threshold?: number | number[]
  root?: Element | null
  rootMargin?: string
  freezeOnceVisible?: boolean
}

export function useIntersectionObserver<T extends Element>(
  options: IntersectionOptions = {}
): [RefObject<T>, boolean, IntersectionObserverEntry | null] {
  const {
    threshold = 0,
    root = null,
    rootMargin = '0px',
    freezeOnceVisible = false,
  } = options

  const ref = useRef<T>(null)
  const [isVisible, setIsVisible] = useState(false)
  const [entry, setEntry] = useState<IntersectionObserverEntry | null>(null)

  useEffect(() => {
    const element = ref.current
    if (!element) return

    // Skip if frozen and already visible
    if (freezeOnceVisible && isVisible) return

    const observer = new IntersectionObserver(
      ([entry]) => {
        setEntry(entry)
        setIsVisible(entry.isIntersecting)
      },
      { threshold, root, rootMargin }
    )

    observer.observe(element)

    return () => {
      observer.disconnect()
    }
  }, [threshold, root, rootMargin, freezeOnceVisible, isVisible])

  return [ref, isVisible, entry]
}

// Usage example:
function LazyImage({ src, alt }: { src: string; alt: string }) {
  const [ref, isVisible] = useIntersectionObserver<HTMLDivElement>({
    threshold: 0.1,
    freezeOnceVisible: true,
  })

  return (
    <div ref={ref} className="min-h-[200px]">
      {isVisible ? (
        <img src={src} alt={alt} className="animate-fade-in" />
      ) : (
        <div className="bg-gray-200 animate-pulse h-full" />
      )}
    </div>
  )
}

Looking for more?

Browse the full collection of patterns or check out other exploration topics.