npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@usefy/use-intersection-observer

v0.0.38

Published

A React hook for observing element visibility using Intersection Observer API with enterprise-grade features

Downloads

971

Readme


Overview

@usefy/use-intersection-observer is a feature-rich React hook for efficiently detecting element visibility in the viewport using the Intersection Observer API. It provides a simple API for lazy loading, infinite scroll, scroll animations, and more.

Part of the @usefy ecosystem — a collection of production-ready React hooks designed for modern applications.

Why use-intersection-observer?

  • Zero Dependencies — Pure React implementation with no external dependencies
  • TypeScript First — Full type safety with comprehensive type definitions
  • Efficient Detection — Leverages native Intersection Observer API for optimal performance
  • Threshold-based Callbacks — Fine-grained visibility ratio tracking with multiple thresholds
  • TriggerOnce Support — Perfect for lazy loading patterns
  • Dynamic Enable/Disable — Conditional observation support
  • Custom Root Containers — Observe elements within custom scroll containers
  • Root Margin Support — Expand or shrink detection boundaries
  • SSR Compatible — Works seamlessly with Next.js, Remix, and other SSR frameworks
  • Optimized Re-renders — Only updates when meaningful values change
  • Well Tested — Comprehensive test coverage with Vitest

Installation

# npm
npm install @usefy/use-intersection-observer

# yarn
yarn add @usefy/use-intersection-observer

# pnpm
pnpm add @usefy/use-intersection-observer

Peer Dependencies

This package requires React 18 or 19:

{
  "peerDependencies": {
    "react": "^18.0.0 || ^19.0.0"
  }
}

Quick Start

import { useIntersectionObserver } from "@usefy/use-intersection-observer";

function MyComponent() {
  const { ref, inView, entry } = useIntersectionObserver();

  return <div ref={ref}>{inView ? "👁️ Visible!" : "👻 Not visible"}</div>;
}

API Reference

useIntersectionObserver(options?)

A hook that observes element visibility using the Intersection Observer API.

Parameters

| Parameter | Type | Description | | --------- | -------------------------------- | ----------------------------- | | options | UseIntersectionObserverOptions | Optional configuration object |

Options

| Option | Type | Default | Description | | ----------------------- | ----------------------------------------------------- | ------- | --------------------------------------------------------------------------------------------------- | | threshold | number \| number[] | 0 | Visibility ratio(s) that trigger updates (0.0 to 1.0). Updates occur when crossing these boundaries | | root | Element \| Document \| null | null | Root element for intersection (null = viewport) | | rootMargin | string | "0px" | Margin around root (CSS margin syntax). Positive expands, negative shrinks detection area | | triggerOnce | boolean | false | Stop observing after element first becomes visible | | enabled | boolean | true | Enable/disable observer. When false, observer disconnects and stops all updates | | initialIsIntersecting | boolean | false | Initial intersection state before first observation (useful for SSR/SSG) | | onChange | (entry: IntersectionEntry, inView: boolean) => void | — | Callback fired when isIntersecting or intersectionRatio changes | | delay | number | 0 | Delay in milliseconds before creating the observer (not individual events) |

Returns UseIntersectionObserverReturn

| Property | Type | Description | | -------- | --------------------------------- | ------------------------------------------------------------------------------ | | entry | IntersectionEntry \| null | Intersection entry data (null if not yet observed). Updates trigger re-renders | | inView | boolean | Whether the element is currently intersecting (convenience derived from entry) | | ref | (node: Element \| null) => void | Callback ref to attach to the target element you want to observe |

IntersectionEntry

Extended intersection entry with convenience properties:

| Property | Type | Description | | -------------------- | --------------------------- | ---------------------------------------------------------------- | | entry | IntersectionObserverEntry | Original native IntersectionObserverEntry from the browser API | | isIntersecting | boolean | Whether target is intersecting with root | | intersectionRatio | number | Ratio of target visible (0.0 to 1.0) | | target | Element | The observed DOM element | | boundingClientRect | DOMRectReadOnly | Target element's bounding box relative to viewport | | intersectionRect | DOMRectReadOnly | Visible portion's bounding box (intersection of target and root) | | rootBounds | DOMRectReadOnly \| null | Root element's bounding box (null if root is the viewport) | | time | number | DOMHighResTimeStamp when intersection was recorded |


Examples

Basic Usage

import { useIntersectionObserver } from "@usefy/use-intersection-observer";

function VisibilityChecker() {
  const { ref, inView } = useIntersectionObserver();

  return <div ref={ref}>{inView ? "👁️ Visible!" : "👻 Not visible"}</div>;
}

Lazy Loading Images

import { useState } from "react";
import { useIntersectionObserver } from "@usefy/use-intersection-observer";

function LazyImage({ src, alt }: { src: string; alt: string }) {
  const [loaded, setLoaded] = useState(false);

  const { ref, inView } = useIntersectionObserver({
    triggerOnce: true, // Stop observing after first detection
    threshold: 0.1, // Trigger when 10% visible
    rootMargin: "50px", // Start loading 50px before entering viewport
  });

  return (
    <div ref={ref}>
      {inView ? (
        <img
          src={src}
          alt={alt}
          onLoad={() => setLoaded(true)}
          style={{ opacity: loaded ? 1 : 0 }}
        />
      ) : (
        <div className="placeholder">Loading...</div>
      )}
    </div>
  );
}

Infinite Scroll

import { useState, useEffect } from "react";
import { useIntersectionObserver } from "@usefy/use-intersection-observer";

function InfiniteList() {
  const [items, setItems] = useState([...initialItems]);
  const [loading, setLoading] = useState(false);

  const { ref, inView } = useIntersectionObserver({
    threshold: 1.0, // Trigger when sentinel is fully visible
    rootMargin: "100px", // Start loading 100px before sentinel enters viewport
  });

  useEffect(() => {
    if (inView && !loading) {
      setLoading(true);
      fetchMoreItems().then((newItems) => {
        setItems((prev) => [...prev, ...newItems]);
        setLoading(false);
      });
    }
  }, [inView, loading]);

  return (
    <div>
      {items.map((item) => (
        <Item key={item.id} {...item} />
      ))}
      {/* Sentinel Element - triggers loading when visible */}
      <div ref={ref}>{loading && <Spinner />}</div>
    </div>
  );
}

Scroll Animations

import { useIntersectionObserver } from "@usefy/use-intersection-observer";

function AnimatedCard({ children }: { children: React.ReactNode }) {
  const { ref, inView } = useIntersectionObserver({
    triggerOnce: true, // Animate only once
    threshold: 0.3, // Trigger when 30% visible
  });

  return (
    <div
      ref={ref}
      style={{
        opacity: inView ? 1 : 0,
        transform: inView ? "translateY(0)" : "translateY(30px)",
        transition: "all 0.6s ease",
      }}
    >
      {children}
    </div>
  );
}

Reading Progress Tracker

import { useState } from "react";
import { useIntersectionObserver } from "@usefy/use-intersection-observer";

function ProgressTracker() {
  const [progress, setProgress] = useState(0);

  // 101 thresholds (0%, 1%, 2%, ... 100%) for fine-grained tracking
  const thresholds = Array.from({ length: 101 }, (_, i) => i / 100);

  const { ref } = useIntersectionObserver({
    threshold: thresholds,
    onChange: (entry) => {
      // Update progress when crossing any threshold boundary
      setProgress(Math.round(entry.intersectionRatio * 100));
    },
  });

  return (
    <>
      <div className="progress-bar" style={{ width: `${progress}%` }} />
      <article ref={ref}>{/* Long content */}</article>
    </>
  );
}

Custom Scroll Container

import { useRef } from "react";
import { useIntersectionObserver } from "@usefy/use-intersection-observer";

function ScrollContainer() {
  const containerRef = useRef<HTMLDivElement>(null);

  const { ref, inView } = useIntersectionObserver({
    root: containerRef.current,
    rootMargin: "0px",
  });

  return (
    <div ref={containerRef} style={{ overflow: "auto", height: 400 }}>
      <div style={{ height: 1000 }}>
        <div ref={ref}>{inView ? "Visible in container" : "Not visible"}</div>
      </div>
    </div>
  );
}

Section Navigation Highlighting

import { useState } from "react";
import { useIntersectionObserver } from "@usefy/use-intersection-observer";

function SectionNavigation() {
  const [activeSection, setActiveSection] = useState<string | null>(null);

  return (
    <>
      <nav>
        {sections.map((section) => (
          <button
            key={section.id}
            className={activeSection === section.id ? "active" : ""}
          >
            {section.name}
          </button>
        ))}
      </nav>

      {sections.map((section) => (
        <Section
          key={section.id}
          id={section.id}
          onVisible={() => setActiveSection(section.id)}
        />
      ))}
    </>
  );
}

function Section({ id, onVisible }: { id: string; onVisible: () => void }) {
  const { ref } = useIntersectionObserver({
    threshold: 0.6, // Activate when 60% visible
    onChange: (_, inView) => {
      // Called when section enters or exits the 60% visibility threshold
      if (inView) onVisible();
    },
  });

  return (
    <section ref={ref} id={id}>
      ...
    </section>
  );
}

Dynamic Enable/Disable

import { useState } from "react";
import { useIntersectionObserver } from "@usefy/use-intersection-observer";

function ConditionalObserver() {
  const [isLoading, setIsLoading] = useState(true);

  const { ref, inView } = useIntersectionObserver({
    enabled: !isLoading, // Observer is disconnected when disabled
  });

  return <div ref={ref}>{inView ? "Observing" : "Not observing"}</div>;
}

SSR/SSG Support

import { useIntersectionObserver } from "@usefy/use-intersection-observer";

function SSRComponent() {
  // Set initial state for server-side rendering
  const { ref, inView } = useIntersectionObserver({
    initialIsIntersecting: true, // Assume visible during SSR
  });

  // During SSR/first render, inView will be true
  // After hydration, actual intersection state takes over
  return <div ref={ref}>{inView ? "Initially visible" : "Not visible"}</div>;
}

Delay Observer Creation

import { useIntersectionObserver } from "@usefy/use-intersection-observer";

function DelayedObserver() {
  const { ref, inView } = useIntersectionObserver({
    delay: 500, // Wait 500ms before creating the observer
  });

  // Observer is NOT created until 500ms after component mount
  // This delays the CREATION of the observer, not individual intersection events
  // Useful for preventing premature observations during page load or fast scrolling
  return <div ref={ref}>{inView ? "Observing" : "Not observing"}</div>;
}

Performance Optimization

The Intersection Observer API fires callbacks when threshold boundaries are crossed or when isIntersecting changes (e.g., during user scroll interactions). When a callback fires:

  • The entry object is updated with new values including time (timestamp of the intersection event)
  • setEntry() is called → re-render occurs

The hook includes a safeguard: it compares the previous isIntersecting and intersectionRatio values with the new ones before calling setEntry(). This prevents redundant re-renders in edge cases where the observer might report the same state multiple times.

// Inside the hook's callback:
const hasChanged =
  !prevEntry ||
  prevEntry.isIntersecting !== nativeEntry.isIntersecting ||
  prevEntry.intersectionRatio !== nativeEntry.intersectionRatio;

if (hasChanged) {
  setEntry(intersectionEntry); // Re-render triggered
}

TypeScript

This hook is written in TypeScript and exports comprehensive type definitions.

import {
  useIntersectionObserver,
  type UseIntersectionObserverOptions,
  type UseIntersectionObserverReturn,
  type IntersectionEntry,
  type OnChangeCallback,
} from "@usefy/use-intersection-observer";

// Full type inference
const { ref, inView, entry }: UseIntersectionObserverReturn =
  useIntersectionObserver({
    threshold: 0.5,
    onChange: (entry, inView) => {
      console.log("Visibility changed:", inView);
    },
  });

Performance

  • Stable Function References — The ref callback is memoized with useCallback
  • Smart Re-renders — Only re-renders when isIntersecting or intersectionRatio changes
  • Native API — Leverages browser's Intersection Observer API for optimal performance
  • SSR Compatible — Gracefully degrades in server environments
const { ref } = useIntersectionObserver({
  threshold: [0, 0.5, 1.0],
});

// ref reference remains stable across renders
useEffect(() => {
  // Safe to use as dependency
}, [ref]);

Browser Support

This hook uses the Intersection Observer API, which is supported in:

  • Chrome 51+
  • Firefox 55+
  • Safari 12.1+
  • Edge 15+
  • Opera 38+

For unsupported browsers, the hook gracefully degrades and returns the initial state.


Testing

This package maintains comprehensive test coverage to ensure reliability and stability.

Test Coverage

📊 View Detailed Coverage Report (GitHub Pages)

Test Files

  • useIntersectionObserver.test.ts — 87 tests for hook behavior
  • utils.test.ts — 63 tests for utility functions

Total: 150 tests


License

MIT © mirunamu

This package is part of the usefy monorepo.