Skip to main content

Overview

The useIntersectionObserver hook provides a simple way to detect when an element enters or exits the viewport. This is useful for implementing lazy loading, infinite scroll, animations on scroll, and other viewport-based interactions.

Function Signature

const useIntersectionObserver = (options?: IntersectionObserverInit) => {
  ref: RefObject<HTMLDivElement>
  isVisible: boolean
}

Parameters

options
IntersectionObserverInit
Configuration options for the Intersection Observer. The hook provides sensible defaults but allows full customization.Default values:
  • root: null (uses viewport)
  • rootMargin: '400px' (triggers 400px before element enters viewport)
  • threshold: 0.1 (triggers when 10% of element is visible)
Any options you provide will override these defaults.

IntersectionObserverInit Properties

options.root
Element | null
The element used as the viewport for checking visibility. Defaults to the browser viewport if null.
options.rootMargin
string
Margin around the root element. Can be used to grow or shrink the intersection area. Accepts CSS-like values (e.g., "10px", "10px 20px").
options.threshold
number | number[]
A single number or array of numbers between 0 and 1, indicating at what percentage of the target’s visibility the observer’s callback should execute.

Return Values

ref
RefObject<HTMLDivElement>
A React ref object that should be attached to the element you want to observe. The hook monitors this element’s visibility.
isVisible
boolean
A boolean indicating whether the observed element is currently intersecting with the viewport (or root element). Updates automatically as the element enters or exits the viewport.

Usage Example

Here’s a real-world example from the Bookmark component (/home/daytona/workspace/source/src/components/Bookmark.tsx:16) implementing lazy loading:
import useIntersectionObserver from "../hooks/useIntersectionObserver"

const Bookmark = ({ bookmark }) => {
  const { ref, isVisible } = useIntersectionObserver()

  return (
    <div>
      <div ref={ref} className="aspect-[1.91/1] bg-zinc-800">
        {isVisible && (
          <img 
            src={bookmark.image} 
            alt={bookmark.title} 
            loading="lazy"
            className="object-cover size-full animate-fade-in"
          />
        )}
      </div>
      <div>
        {isVisible && (
          <img 
            src={`https://icon.horse/icon/${bookmark.domain}`} 
            alt={`${bookmark.title} icon`}
          />
        )}
        <p>{bookmark.title}</p>
      </div>
    </div>
  )
}

Custom Options Example

// Trigger when element is exactly in viewport (no margin)
const { ref, isVisible } = useIntersectionObserver({
  rootMargin: '0px',
  threshold: 0.5 // Trigger when 50% visible
})

// Trigger at multiple thresholds
const { ref, isVisible } = useIntersectionObserver({
  threshold: [0, 0.25, 0.5, 0.75, 1.0]
})

// Use a specific container as viewport
const containerRef = useRef<HTMLDivElement>(null)
const { ref, isVisible } = useIntersectionObserver({
  root: containerRef.current
})

Implementation Details

The hook:
  • Creates an IntersectionObserver instance with merged default and custom options
  • Observes the element referenced by the returned ref
  • Updates the isVisible state when the element’s intersection status changes
  • Automatically cleans up the observer when the component unmounts
  • Re-creates the observer if options change

Common Use Cases

  • Lazy loading images: Load images only when they’re about to enter the viewport
  • Infinite scroll: Load more content when user scrolls near the bottom
  • Animations on scroll: Trigger animations when elements become visible
  • Analytics: Track when users view specific sections
  • Performance optimization: Defer rendering of expensive components

Performance Considerations

The default rootMargin of '400px' means elements will be considered visible 400px before they enter the viewport. This provides a buffer for lazy loading, ensuring images load before users see them.
For better performance with many observed elements, consider using a single shared observer instead of creating multiple instances. However, this hook’s approach is suitable for most use cases.

Browser Compatibility

The Intersection Observer API is widely supported in modern browsers but may require a polyfill for older browsers. Check Can I Use for current compatibility.

Advanced Example: Fade-in Animation

const AnimatedSection = ({ children }) => {
  const { ref, isVisible } = useIntersectionObserver({
    threshold: 0.2,
    rootMargin: '0px'
  })

  return (
    <div 
      ref={ref}
      className={`transition-opacity duration-1000 ${
        isVisible ? 'opacity-100' : 'opacity-0'
      }`}
    >
      {children}
    </div>
  )
}

Build docs developers (and LLMs) love