mwui
GitHub

useIntersectionObserver

A hook that tracks whether an element is in the viewport.

Card in viewhidden
Scroll down ↓
Watched card
Keep scrolling ↓
Revealed once

Installation

Usage

The primitive behind infinite scroll, lazy media, reveal-on-scroll animation, and scroll-spy navigation — all of which are the same question asked with different options.

import { useIntersectionObserver } from "@/hooks/use-intersection-observer";

function LoadMore({ onReach }: { onReach: () => void }) {
  const { ref } = useIntersectionObserver({
    rootMargin: "400px",
    onChange: (entry) => entry.isIntersecting && onReach(),
  });

  return <div ref={ref} aria-hidden className="h-px" />;
}

ref is a callback ref, so the element can mount, unmount, and remount — inside a conditional, a list, or a virtualized viewport — and the observer follows it. A RefObject cannot do that.

Reveal once

once freezes the result after the first intersection and disconnects the observer, which is what entrance animations want: they should not replay when the user scrolls back up.

const { ref, isIntersecting } = useIntersectionObserver({ threshold: 0.3, once: true });

<section ref={ref} data-visible={isIntersecting} className="opacity-0 data-[visible=true]:animate-in">

Options

OptionDescription
rootScroll container to measure against. Defaults to the viewport.
rootMarginGrows or shrinks the root box. Positive values fire early — use for prefetch.
thresholdFraction of the element that must be visible. Accepts an array.
onceStop observing after the first intersection.
disabledPause observation without unmounting.
initialIsIntersectingValue returned before the observer reports. Set true to avoid a flash.
onChangeCalled with each entry. Read from a ref, so it need not be memoized.

Returns

entry is the raw IntersectionObserverEntry — reach for intersectionRatio or boundingClientRect when a boolean is not enough, such as a scroll-spy that highlights the most visible heading.