Skip to content

shadcn.io is an independent project built on shadcn/ui

Docs

Free

useIntersectionObserver for React

Reports whether an element is in view, with the observer entry and a ref callback. For React, when you lazy load, infinite scroll or animate once.

useIntersectionObserver tells you when an element enters or leaves view.

Attach the ref to the node you care about and read isIntersecting. Set freezeOnceVisible when a lazy image should load once and then stop watching. If you only need a boolean you can flip by hand, use useBoolean. The observer is created in an effect, so React does not touch it while rendering on the server.

Preview width

Installation

Sign in to get your install command
Free with an account. It carries your token, so the CLI installs everything in one go.

Anatomy

useIntersectionObserver anatomy: useIntersectionObserver(), isIntersecting, ref, threshold

Usage

import {
  useIntersectionObserver,
} from "@/hooks/use-intersection-observer";
const { ref, isIntersecting } = useIntersectionObserver({
  threshold: 0.5,
});

ref is a callback. isIntersecting stays false until the element crosses the threshold. The third value, also named entry, is the latest IntersectionObserverEntry.

FAQ

How do I lazy load an image with useIntersectionObserver?

Set freezeOnceVisible to true and attach ref to a placeholder near the image. The first time the element crosses your threshold, isIntersecting stays true and the effect stops observing. Swap in the real image when that flag becomes true. You do not need to disconnect the observer yourself. Leaving the page still runs the effect cleanup.

What does the threshold option do?

Threshold is how much of the element must be visible before isIntersecting becomes true. 0 means any overlap with the root counts, 0.5 means half of the element, and 1 means the whole element. You can pass an array when you want a callback at several ratios. The hook treats a tick as intersecting only when the entry is intersecting and its ratio is at least one of those numbers.

How do I run the callback before the element is on screen?

Pass rootMargin to grow or shrink the root box used for the test. A value like 100px makes the element count as intersecting while it is still 100 pixels outside the viewport, which is useful when you want to preload the next page or image. Negative margins shrink the box, so the element must sit further inside before the flag turns true. The string follows the CSS margin shorthand.

Can I use this hook for infinite scroll?

Yes. Render a short sentinel after the last item and point ref at it. When isIntersecting becomes true, append the next page. Keep the sentinel at the end of the new list so it can intersect again. If a request is already in flight, ignore later ticks until it finishes. The hook does not fetch for you. It only reports that the sentinel entered the root.

Is useIntersectionObserver safe during server rendering?

Yes. The observer is created inside an effect, so the server never constructs one. The first render uses initialIsIntersecting, which defaults to false, on both the server and the client. Set that option to true only when you want the first paint to look visible. If the browser has no IntersectionObserver, the effect returns without changing state.

Why does the hook return an array and named fields?

The return value is a three-item tuple and an object at the same time. Position 0 is the ref callback, 1 is the boolean, and 2 is the latest IntersectionObserverEntry. The same values also sit on ref, isIntersecting, and entry, so you can destructure the names. entry is undefined until the first observation. Both styles read the same state.

API Reference

Parameters

ParameterType
options?UseIntersectionObserverOptions

What counts as in view, and whether to stop once it is.

options.freezeOnceVisible?boolean

Stops observing once the element is first in view, so it stays true: for an animation that plays once, or content that loads once.

Default
false
options.initialIsIntersecting?boolean

The value before the first observation (and on the server).

Default
false
options.onChange?(isIntersecting: boolean, entry: IntersectionObserverEntry) => void

Called on every change, with whether it's in view and the entry.

Type
(isIntersecting: boolean, entry: IntersectionObserverEntry) => void
options.root?Document | Element | null

The element (or document) it's in view of. The viewport when null.

Type
Document | Element | null
Default
null
options.rootMargin?string

Grows or shrinks the root's box before checking, in CSS margin syntax: "200px" counts it in view 200px early.

Default
"0%"
options.threshold?number | number[]

How much of the element must show to count: 0 is any of it, 1 all of it. An array reports at each.

Type
number | number[]
Default
0

Returns

ValueType
[0] ref(node?: Element | null) => void

Pass to the element's ref: a callback ref, so it can change.

Type
(node?: Element | null) => void
[1] isIntersectingboolean

Whether the element is in view, past the threshold.

[2] entryIntersectionObserverEntry | undefined

The latest observation, undefined before the first.

Type
IntersectionObserverEntry | undefined
entry?IntersectionObserverEntry

The latest observation, undefined before the first.

isIntersectingboolean

Whether the element is in view, past the threshold.

ref(node?: Element | null) => void

Pass to the element's ref: a callback ref, so it can change.

Type
(node?: Element | null) => void