Skip to content
Esc
navigateopen⌘Jpreview
On this page

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.

Was this page helpful?