A thin wrapper around the IntersectionObserver API. It observes a target element and fires a callback when the element intersects with the viewport. Includes a graceful fallback for environments where the API isn’t available, plus a stop() function to disconnect the observer when you’re done.

export interface IntersectionObserverOptions {
  /**
   * The Element or Document whose bounds are used as the bounding box when testing for intersection.
   * The element that is used as the viewport for checking visibility of the target.
   * Must be the ancestor of the target. Defaults to the browser viewport if not specified or if null.
   */
  root?: Element | Document

  /**
   * A string which specifies a set of offsets to add to the root's bounding_box when calculating intersections.
   */
  rootMargin?: string

  /**
   * Either a single number or an array of numbers between 0.0 and 1.
   * A threshold of 1.0 means that when 100% of the target is visible within the element specified by the root option, the callback is invoked.
   */
  threshold?: number | number[]
}

export function useIntersectionObserver(
  target: Element,
  callback: IntersectionObserverCallback,
  options: IntersectionObserverOptions = {},
): { isSupported: boolean; stop: () => void } {
  const { root, rootMargin = '0px', threshold = 0.1 } = options
  const isSupported = typeof window !== 'undefined' && 'IntersectionObserver' in window

  if (!isSupported) {
    return { isSupported, stop: () => {} }
  }

  const observer = new IntersectionObserver(callback, {
    root,
    rootMargin,
    threshold,
  })

  observer.observe(target)

  return {
    isSupported,
    stop: () => {
      observer.disconnect()
    },
  }
}

Usage example

Watching .target-element and logging when it enters or leaves the viewport:

import { useIntersectionObserver } from './use-intersection-observer'

const targetElement = document.querySelector('.target-element')

const callback: IntersectionObserverCallback = (entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      console.log('Element is in the viewport')
    } else {
      console.log('Element is out of the viewport')
    }
  })
}

if (targetElement instanceof Element) {
  const { stop } = useIntersectionObserver(targetElement, callback, { threshold: 0.2 })

  // Call stop() when you no longer need observation.
  // stop()
}