DOM
useIntersectionObserver
Observe an element's viewport intersection reactively.
Demo
live · imported from hookli
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.
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>
);
}Usage
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(options?: UseIntersectionObserverOptions): UseIntersectionObserverReturnParameters
| Name | Type | Default | Description |
|---|---|---|---|
| options | UseIntersectionObserverOptions | {} | Observer thresholds, root, and behaviour flags. All optional. |
Returns
| Name | Type | Description |
|---|---|---|
| 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. |
UseIntersectionObserverOptions
Configures the underlying IntersectionObserver.
| Name | Type | Description |
|---|---|---|
| threshold | number | number[] | One or more visibility ratios at which to fire. |
| root | Element | Document | null | The element used as the viewport. Defaults to the browser viewport. |
| rootMargin | string | Margin around the root, in CSS-margin syntax — grows or shrinks the trigger area. |
| freezeOnceVisible | boolean | Once the target is visible, stop observing and keep the visible state. |
| initialIsIntersecting | boolean | 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.
| Name | Type | Description |
|---|---|---|
| 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. |
Hook
The implementation, straight from hookli. Copy it, or install the package.
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 };
};