Skip to content
Esc
navigateopen⌘Jpreview

Know when an element meets the viewport.

Reveal on scroll, lazy-load images, track impressions, build infinite lists. A tiny, fully-typed React adapter for the Intersection Observer API, in about a kilobyte.

$ npm i react-intersection-observer
  • v11
  • MIT
  • TypeScript
  • ~1 kB gzipped
  • React 17+
observerlive
threshold
Hero
section
Hero
intersectionRatio
1.00
inView
true
impressions
0

This section revealed itself.

Every card below waited off-screen and animated in as it crossed the threshold, with the same useInView you would ship. It fires once and then leaves the DOM alone.

Hooks or component

useInView for state, useOnInView for effects, and <InView> for render props or a wrapper.

About a kilobyte

Around 1 kB gzipped per API. Tree-shakeable, so you ship only what you import.

Matches the native API

The same threshold, rootMargin, and root options you already know from IntersectionObserver.

Shared instances

Elements with identical options reuse one observer, so thousands of targets stay cheap.

Typed to the core

Written in TypeScript. Options, return values, and entries are typed, with no extra @types.

Ready to test

A drop-in mock for the Intersection Observer keeps Vitest and Jest suites deterministic.

useInView({ triggerOnce: true })drives every reveal on this page.

Three APIs, one observer.

Pick the shape that fits your component. The panel follows whichever you are reading. That is useInView doing scrollspy, right here. Tap a card to pin one.

useInView
const { ref, inView } = useInView({  threshold: 0.5,});<section ref={ref}>  {inView ? "In view" : "Waiting"}</section>
  1. const { ref, inView } = useInView({  threshold: 0.5,});<section ref={ref}>  {inView ? "In view" : "Waiting"}</section>
  2. const ref = useOnInView(  (inView, entry) => {    track("seen", entry.target);  },  { threshold: 1, triggerOnce: true },);
  3. <InView as="div" threshold={0.2} triggerOnce>  {({ ref, inView }) => (    <div ref={ref}>      {inView ? "Loaded" : "Placeholder"}    </div>  )}</InView>

Load it just before it is needed.

Every tile below reserves its space, then mounts its <img> once the observer says it is within 200px of the viewport. Nothing above the fold waits on artwork that is still three screens away.

hero.jpgdeferred
cover.jpgdeferred
avatar.jpgdeferred
chart.jpgdeferred
banner.jpgdeferred
thumb.jpgdeferred
Plain images want loading="lazy" instead. Reach for the observer when you need the preload margin, a placeholder, or a transition.

Fire once, exactly when seen.

useOnInView runs your callback without a hook-owned re-render. It is the right tool for impressions, prefetching, and logging. Each tile below logs itself the first time it is fully visible.

card-01waiting
card-02waiting
card-03waiting
card-04waiting
card-05waiting
card-06waiting
card-07waiting
card-08waiting

Try it yourself.

Change the threshold and scroll the custom root. Same hook, wired to live controls and a live readout.

See when an element becomes visible
Step 2 · Scroll this panel to reveal the observed card ↓
Feed item 1A small feed item
Feed item 2A small feed item
Feed item 3A small feed item
Feed item 4A small feed item
Feed item 5A small feed item
Feed item 6A small feed item
QueuedWatching
Feature card is waiting

Scroll until enough of this card is visible.

Waiting for visibility
Feed item 7A small feed item
Feed item 8A small feed item
Feed item 9A small feed item

Add a kilobyte. Know what is on screen.

Install, attach a ref, and read inView. Thresholds, roots, and margins are there when you need them.

$ npm i react-intersection-observer

Testing? The package ships a mock for the Intersection Observer so your suites stay deterministic.