useIntersectionObserver

React hook to observe element viewport visibility using Intersection Observer with threshold triggers and enter/leave callbacks.

pnpm add react-callback-hooks

Demo

Scroll down inside the boxhidden
scroll down
target · entered 0x
target is hidden

Usage

Simple — single callback fires on every intersection change:

import { useIntersectionObserver } from 'react-callback-hooks'

const [ref, isVisible] = useIntersectionObserver((entry) => {
  console.log('intersection changed', entry.isIntersecting)
})

<div ref={ref} />

Object form — separate enter/leave callbacks with full options:

const [ref, isVisible] = useIntersectionObserver({
  onEnter: (entry) => console.log('entered viewport'),
  onLeave: (entry) => console.log('left viewport'),
  threshold: 0.5,
  rootMargin: '-64px 0px 0px 0px',
  once: true,
})

<section ref={ref} />

Parameters

Simple form

ParameterTypeDescription
callback(entry: IntersectionObserverEntry) => voidFires on every intersection change — both enter and leave.

Object form

PropertyTypeDefaultDescription
onEnter(entry) => voidCalled when the element enters the viewport.
onLeave(entry) => voidCalled when the element leaves the viewport.
thresholdnumber | number[]0Intersection ratio(s) at which callbacks fire.
rootMarginstring'0px'Margin around the root, same syntax as CSS margin.
rootElement | nullnullAncestor used as viewport. Defaults to the browser viewport.
oncebooleanfalseIf true, stops observing after the first onEnter fires.

Return Values

Returns a tuple [ref, isIntersecting]:

TypeDescription
refReact.RefObject<T>Attach to the element you want to observe.
isIntersectingbooleantrue while the observed element is intersecting the viewport.

Lazy loading example

const [ref, isVisible] = useIntersectionObserver({
  onEnter: () => loadImage(),
  once: true,
  threshold: 0.1,
})

<img ref={ref} src={isVisible ? src : undefined} />

Animate on scroll

const [ref, isVisible] = useIntersectionObserver({ threshold: 0.2 })

<div
  ref={ref}
  className={`transition-opacity duration-500 ${isVisible ? 'opacity-100' : 'opacity-0'}`}
/>