# useIntersectionObserver > Observe an element's viewport intersection reactively. - Category: DOM - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-intersection-observer ## Signature ```ts useIntersectionObserver(options?: UseIntersectionObserverOptions): UseIntersectionObserverReturn ``` ## Parameters - `options`: `UseIntersectionObserverOptions` (default: `{}`) — Observer thresholds, root, and behaviour flags. All optional. ## Returns - `ref`: `(node: Element | null) => void` — Ref callback to attach to the element you want to observe. - `isIntersecting`: `boolean` — Whether the observed element currently intersects the root. - `entry`: `IntersectionObserverEntry | null` — The most recent observer entry (intersectionRatio, boundingClientRect…), or null before the first report. ## Types ### UseIntersectionObserverOptions Configures the underlying IntersectionObserver. - `threshold`: `number | number[]` (default: `0`) — One or more visibility ratios at which to fire. - `root`: `Element | Document | null` (default: `null`) — The element used as the viewport. Defaults to the browser viewport. - `rootMargin`: `string` (default: `"0%"`) — Margin around the root, in CSS-margin syntax — grows or shrinks the trigger area. - `freezeOnceVisible`: `boolean` (default: `false`) — Once the target is visible, stop observing and keep the visible state. - `initialIsIntersecting`: `boolean` (default: `false`) — isIntersecting value used before the observer first reports. - `onChange`: `(isIntersecting: boolean, entry: IntersectionObserverEntry) => void` — Called with the latest entry whenever intersection changes. ### UseIntersectionObserverReturn The ref callback plus the current intersection state. - `ref`: `(node: Element | null) => void` — Attach to the element you want to observe. - `isIntersecting`: `boolean` — Whether the target currently intersects the root. - `entry`: `IntersectionObserverEntry | null` — The most recent observer entry, or null before the first report. ## Usage ```tsx import { useIntersectionObserver } from "hookli"; export function Demo() { const { ref, isIntersecting } = useIntersectionObserver({ threshold: 0.5, }); return (
{isIntersecting ? "In view" : "Scroll me into view"}
); } ``` ## Source `src/hooks/use-intersection-observer/use-intersection-observer.ts` ```ts 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(null); const [isIntersecting, setIsIntersecting] = useState(initialIsIntersecting); const [entry, setEntry] = useState(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 }; }; ```