---
title: Observer options
description: 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:

```tsx
useInView({ threshold: 0.5 });
```

Use `rootMargin` to start earlier or later than the target's actual edge. This is useful for preloading:

```tsx
useInView({
  rootMargin: "200px 0px",
  triggerOnce: true,
});
```

`scrollMargin` grows or shrinks the clipping rectangles of scroll containers nested inside the root.

## 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:

```tsx
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>
  );
}
```

When an inner scroller clips the target, add `scrollMargin`, 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.

:::note[Verify configuration]
Test real layout, scrolling, and geometry in [Vitest Browser Mode](/testing/browser-mode). To cross a configured threshold without depending on layout, use a deterministic mock. See [Mock intersections](/testing/mocking-intersections).
:::

## 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`](/api/core-apis#useoninview) when the callback is all you need.

```tsx
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](/guides/ssr) 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 `100`ms. |

### 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:

```tsx
useInView({ trackVisibility: true, delay: 100 });
```

Browser support for v2 is narrower than for the original API. Read [Observer v2](/guides/intersection-observer-v2) before you base viewability or occlusion decisions on `isVisible`.
