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

react-atom-trigger

v2.0.3

Published

Geometry-based scroll trigger for React with precise enter/leave control. A modern alternative to react-waypoint.

Downloads

995

Readme

react-atom-trigger

codecov bundle size

react-atom-trigger helps with the usual "run some code when this thing enters or leaves view" problem. It is a lightweight React alternative to react-waypoint, written in TypeScript.

v2 is a breaking release

If you are coming from v1.x, please check MIGRATION.md.

If you want to stay on the old API:

# pnpm
pnpm add react-atom-trigger@^1

# npm
npm install react-atom-trigger@^1

# yarn
yarn add react-atom-trigger@^1

Install

# pnpm
pnpm add react-atom-trigger

# npm
npm install react-atom-trigger

# yarn
yarn add react-atom-trigger

How it works

react-atom-trigger uses a mixed approach.

  • Geometry is the real source of truth for enter and leave.
  • IntersectionObserver is only there to wake things up when the browser notices a layout shift.
  • rootMargin logic is handled by the library itself, so it stays consistent and does not depend on native observer quirks.

In practice this means AtomTrigger reacts to:

  • scroll
  • window resize
  • root resize
  • sentinel resize
  • layout shifts that move the observed element even if no scroll event happened

This is the main reason v2 can support custom margin-aware behavior and still react to browser-driven layout changes.

Quick start

import React from 'react';
import { AtomTrigger } from 'react-atom-trigger';

export function Example() {
  return (
    <AtomTrigger
      onEnter={event => {
        console.log('entered', event);
      }}
      onLeave={event => {
        console.log('left', event);
      }}
      rootMargin="0px 0px 160px 0px"
      oncePerDirection
    />
  );
}

If you want an already-visible trigger to behave like a normal first enter, pass fireOnInitialVisible.

import React from 'react';
import { AtomTrigger } from 'react-atom-trigger';

export function RestoredScrollExample() {
  return (
    <AtomTrigger
      fireOnInitialVisible
      onEnter={event => {
        if (event.isInitial) {
          console.log('started visible after load');
          return;
        }

        console.log('entered from scrolling');
      }}
    />
  );
}

Child mode

If you pass one top-level child, AtomTrigger observes that element directly instead of rendering its own sentinel.

import React from 'react';
import { AtomTrigger } from 'react-atom-trigger';

export function HeroTrigger() {
  return (
    <AtomTrigger threshold={0.75} onEnter={() => console.log('hero is mostly visible')}>
      <section style={{ minHeight: 240 }}>Hero content</section>
    </AtomTrigger>
  );
}

This is usually the better mode when threshold should depend on a real element size.

Intrinsic elements such as <div> and <section> work automatically.

If you use a custom component, the ref that AtomTrigger passes down still has to reach a real DOM element:

  • in React 19, the component can receive ref as a prop and pass it through
  • in React 18 and older, use React.forwardRef

If the ref never reaches a DOM node, child mode cannot observe anything.

API

interface AtomTriggerProps {
  onEnter?: (event: AtomTriggerEvent) => void;
  onLeave?: (event: AtomTriggerEvent) => void;
  onEvent?: (event: AtomTriggerEvent) => void;
  children?: React.ReactNode;
  once?: boolean;
  oncePerDirection?: boolean;
  fireOnInitialVisible?: boolean;
  disabled?: boolean;
  threshold?: number;
  root?: Element | null;
  rootRef?: React.RefObject<Element | null>;
  rootMargin?: string | [number, number, number, number];
  className?: string;
}

Props in short

  • onEnter, onLeave, onEvent: trigger callbacks with a rich event payload.
  • children: observe one real child element instead of the internal sentinel.
  • once: allow only the first transition overall.
  • oncePerDirection: allow one enter and one leave.
  • fireOnInitialVisible: emit an initial enter when observation starts and the trigger is already active.
  • disabled: stop observing without unmounting the component.
  • threshold: a number from 0 to 1. It affects enter, not leave.
  • root: use a specific DOM element as the visible area.
  • rootRef: same idea as root, but better when the container is created in JSX. If both are passed, rootRef wins.
  • rootMargin: expand or shrink the effective root. String values use IntersectionObserver-style syntax. A four-number array is treated as [top, right, bottom, left] in pixels.
  • className: applies only to the internal sentinel.

Event payload

type AtomTriggerEvent = {
  type: 'enter' | 'leave';
  isInitial: boolean;
  entry: AtomTriggerEntry;
  counts: {
    entered: number;
    left: number;
  };
  movementDirection: 'up' | 'down' | 'left' | 'right' | 'stationary' | 'unknown';
  position: 'inside' | 'above' | 'below' | 'left' | 'right' | 'outside';
  timestamp: number;
};
type AtomTriggerEntry = {
  target: Element;
  rootBounds: DOMRectReadOnly | null;
  boundingClientRect: DOMRectReadOnly;
  intersectionRect: DOMRectReadOnly;
  isIntersecting: boolean;
  intersectionRatio: number;
  source: 'geometry';
};

The payload is library-owned geometry data. It is not a native IntersectionObserverEntry.

isInitial is true only for the synthetic first enter created by fireOnInitialVisible.

Hooks

For someone who wants everything out-of-the-box, useScrollPosition and useViewportSize are also available.

useScrollPosition(options?: {
  target?: Window | HTMLElement | React.RefObject<HTMLElement | null>;
  passive?: boolean;
  throttleMs?: number;
  enabled?: boolean;
}): { x: number; y: number }
useViewportSize(options?: {
  passive?: boolean;
  throttleMs?: number;
  enabled?: boolean;
}): { width: number; height: number }

Both hooks are SSR-safe. Default throttling is 16ms.

Notes

  • In sentinel mode, threshold is usually only interesting if your sentinel has real width or height. The default sentinel is almost point-like.
  • Child mode needs exactly one top-level child and any custom component used there needs to pass the received ref through to a DOM element.
  • In React 19, a plain function component can also work in child mode if it passes the received ref prop through to a DOM element.
  • rootMargin is handled by the library geometry logic. IntersectionObserver is only used as a wake-up signal for layout shifts.

Migration from v1

The short version:

  1. callback became onEnter, onLeave and onEvent.
  2. behavior is gone.
  3. triggerOnce became once or oncePerDirection.
  4. scrollEvent, dimensions and offset are gone.
  5. useWindowScroll / useContainerScroll became useScrollPosition.
  6. useWindowDimensions became useViewportSize.

For the real upgrade notes and examples, see MIGRATION.md.

Build output

This package is built with tsdown.

lib/index.js
lib/index.umd.js
lib/index.d.ts

When the UMD bundle is loaded directly in the browser, the library is exposed as window.reactAtomTrigger.

Examples

Storybook

Storybook is the easiest way to see how the component behaves.

  • AtomTrigger Demo: regular usage examples.
  • Extended Demo: a larger animated interaction demo that shows AtomTrigger driving scene changes, event timing and more realistic scroll-based UI behavior.
  • Internal Tests: interaction stories used for local checks and Storybook tests.

To run Storybook locally:

pnpm storybook

The latest Storybook build for react-atom-trigger is also available at storybook.atomtrigger.dev.

CodeSandbox

Quick way to tweak it in the browser.

Development

pnpm install
pnpm lint
pnpm test
pnpm test:storybook
pnpm build
pnpm format:check

Storybook (Static Build)

Build:

pnpm build:sb

Output:

storybook-static/

This directory is used for deployment to storybook.atomtrigger.dev.