DOM

useIntersectionObserver

Observe an element's viewport intersection reactively.

Demo

scroll down ↓

Target

↑ scroll up

isIntersecting
false
intersectionRatio
0%

Ideal for lazy-loading, scroll-spy nav and infinite lists — the target crosses the 50% threshold to flip the flag.

Usage

use-intersection-observer.tsxtsx
import { useIntersectionObserver } from "hookli";

export function Demo() {
  const { ref, isIntersecting } = useIntersectionObserver({
    threshold: 0.5,
  });

  return (
    <div ref={ref}>
      {isIntersecting ? "In view" : "Scroll me into view"}
    </div>
  );
}

API

useIntersectionObserver.d.tsts
useIntersectionObserver(options?: UseIntersectionObserverOptions): UseIntersectionObserverReturn

Parameters

NameTypeDefaultDescription
optionsUseIntersectionObserverOptions{}Observer thresholds, root, and behaviour flags. All optional.

Returns

NameTypeDescription
ref(node: Element | null) => voidRef callback to attach to the element you want to observe.
isIntersectingbooleanWhether the observed element currently intersects the root.
entryIntersectionObserverEntry | nullThe most recent observer entry (intersectionRatio, boundingClientRect…), or null before the first report.

UseIntersectionObserverOptions

Configures the underlying IntersectionObserver.

NameTypeDescription
thresholdnumber | number[]One or more visibility ratios at which to fire.
rootElement | Document | nullThe element used as the viewport. Defaults to the browser viewport.
rootMarginstringMargin around the root, in CSS-margin syntax — grows or shrinks the trigger area.
freezeOnceVisiblebooleanOnce the target is visible, stop observing and keep the visible state.
initialIsIntersectingbooleanisIntersecting value used before the observer first reports.
onChange(isIntersecting: boolean, entry: IntersectionObserverEntry) => voidCalled with the latest entry whenever intersection changes.

UseIntersectionObserverReturn

The ref callback plus the current intersection state.

NameTypeDescription
ref(node: Element | null) => voidAttach to the element you want to observe.
isIntersectingbooleanWhether the target currently intersects the root.
entryIntersectionObserverEntry | nullThe most recent observer entry, or null before the first report.

Hook

The implementation, straight from hookli. Copy it, or install the package.

src/hooks/use-intersection-observer/use-intersection-observer.tstsx
import { useCallback, useEffect, useRef, useState } from "react";

interface UseIntersectionObserverOptions {
  threshold?: number | number[];
  root?: Element | Document | null;
  rootMargin?: string;
  freezeOnceVisible?: boolean;
  initialIsIntersecting?: boolean;
  onChange?: (
    isIntersecting: boolean,
    entry: IntersectionObserverEntry,
  ) => void;
}

interface UseIntersectionObserverReturn {
  ref: (node: Element | null) => void;
  isIntersecting: boolean;
  entry: IntersectionObserverEntry | null;
}

export const useIntersectionObserver = ({
  threshold = 0,
  root = null,
  rootMargin = "0%",
  freezeOnceVisible = false,
  initialIsIntersecting = false,
  onChange,
}: UseIntersectionObserverOptions = {}): UseIntersectionObserverReturn => {
  const [element, setElement] = useState<Element | null>(null);
  const [isIntersecting, setIsIntersecting] = useState(initialIsIntersecting);
  const [entry, setEntry] = useState<IntersectionObserverEntry | null>(null);

  const onChangeRef = useRef(onChange);
  onChangeRef.current = onChange;

  const frozen = entry?.isIntersecting && freezeOnceVisible;

  const ref = useCallback((node: Element | null) => {
    setElement(node);
  }, []);

  useEffect(() => {
    if (!element) return;
    if (frozen) return;
    if (typeof IntersectionObserver === "undefined") return;

    const observer = new IntersectionObserver(
      ([observerEntry]) => {
        setEntry(observerEntry);
        setIsIntersecting(observerEntry.isIntersecting);
        onChangeRef.current?.(observerEntry.isIntersecting, observerEntry);
      },
      { threshold, root, rootMargin },
    );

    observer.observe(element);
    return () => {
      observer.disconnect();
    };
  }, [element, JSON.stringify(threshold), root, rootMargin, frozen]);

  return { ref, isIntersecting, entry };
};

View source on GitHub

Related hooks