Observer options
Decide when observation should trigger, where it should happen, and how long it should run.
Most components only need to decide when an element counts as visible, where to observe it, and what happens after the first notification. Those three sections come first. The full option reference is at the end.
Attach ref to the target. The browser viewport is the root by default. An intersection happens when the target crosses that root’s bounds, and thresholds and margins move where that line sits.
Choose when it counts as visible
Use threshold when the amount of visible content matters. A threshold of 0.5 means the target must be at least half visible before inView becomes true:
useInView({ threshold: 0.5 });
Use rootMargin to start earlier or later than the target’s actual edge. This is useful for preloading:
useInView({
rootMargin: "200px 0px",
triggerOnce: true,
});
scrollMargin grows or shrinks the clipping rectangles of nested scroll containers. Reach for it when a scroller inside the root clips the target, not when you want to adjust the viewport.
Choose where to observe
Leave root out, or set it to null, to observe against the browser viewport. To observe inside a scroll container, pass that container as root. It has to be an ancestor of the target, and it needs a bounded scrolling area. Keep it in state so the hook gets the element after React assigns it:
import { useState } from "react";
import { useInView } from "react-intersection-observer";
export function ScrollArea() {
const [root, setRoot] = useState<HTMLDivElement | null>(null);
const { ref, inView } = useInView({ root });
return (
<div
ref={setRoot}
style={{ blockSize: 240, overflowY: "auto" }}
>
<div style={{ blockSize: 220 }} />
<article ref={ref}>
{inView ? "Visible in this container" : "Waiting"}
</article>
<div style={{ blockSize: 220 }} />
</div>
);
}
scrollMargin is not a substitute for rootMargin. It only affects nested scroll containers inside the root, as in useInView({ root, scrollMargin: "80px 0px" }). Watch out for the things that quietly change what the browser considers visible: invalid margin syntax, thresholds outside 0 to 1, iframes, and custom roots.
Stop or pause observation
triggerOnce stops observing after the first accepted true transition. skip disables observation and keeps the current state.
Use onChange with useInView or <InView> when you want both React state and a callback. Use useOnInView when the callback is all you need.
const { ref, inView } = useInView({
triggerOnce: true,
skip: isSaving,
onChange(nextInView, entry) {
console.log(nextInView, entry.target);
},
});
onChange runs alongside the state update.
Initial, server, and unsupported-client state
initialInView sets the state before any observer reports. fallbackInView sets a value only when the client has no IntersectionObserver, and defaultFallbackInView sets that value for the whole application.
Both affect server rendering and unsupported browsers. Decide the policy once in SSR and fallbacks instead of spreading it across individual components.
Option reference
The observation options work with all three APIs. onChange, initialInView, and fallbackInView only work with useInView and <InView>.
| Option | Default | Purpose |
|---|---|---|
root |
null |
The viewport or an ancestor scroll container. |
rootMargin |
"0px" |
Expand or contract the root bounds. |
scrollMargin |
"0px" |
Adjust clipping across nested scroll containers. |
threshold |
0 |
A number or array of ratios from 0 to 1. |
triggerOnce |
false |
Stop observing after the first accepted enter transition. |
skip |
false |
Disable observation while preserving the current state. |
onChange |
undefined |
Run (inView, entry) after an accepted transition. |
initialInView |
false |
Set initial state before observer delivery. |
fallbackInView |
undefined |
Set state when the API is unavailable. |
trackVisibility |
false |
Experimental. Ask browsers that support Observer v2 for entry.isVisible, which goes beyond geometric intersection. |
delay |
undefined |
Minimum delay between v2 visibility notifications. Only valid with trackVisibility, and must be at least 100ms. |
Experimental v2 options
Most applications only need inView, which means the target intersects the root. Set trackVisibility: true when you need Observer v2’s entry.isVisible field instead, which also accounts for an intersecting target being covered or filtered. Pair it with a delay of at least 100 milliseconds:
useInView({ trackVisibility: true, delay: 100 });
Browser support for v2 is narrower than for the original API. Read Observer v2 before you base viewability or occlusion decisions on isVisible.