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
Maintainers
Readme
react-atom-trigger
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@^1Install
# pnpm
pnpm add react-atom-trigger
# npm
npm install react-atom-trigger
# yarn
yarn add react-atom-triggerHow it works
react-atom-trigger uses a mixed approach.
- Geometry is the real source of truth for
enterandleave. IntersectionObserveris only there to wake things up when the browser notices a layout shift.rootMarginlogic 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
refas 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 oneenterand oneleave.fireOnInitialVisible: emit an initialenterwhen observation starts and the trigger is already active.disabled: stop observing without unmounting the component.threshold: a number from0to1. It affectsenter, notleave.root: use a specific DOM element as the visible area.rootRef: same idea asroot, but better when the container is created in JSX. If both are passed,rootRefwins.rootMargin: expand or shrink the effective root. String values useIntersectionObserver-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,
thresholdis 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
refprop through to a DOM element. rootMarginis handled by the library geometry logic.IntersectionObserveris only used as a wake-up signal for layout shifts.
Migration from v1
The short version:
callbackbecameonEnter,onLeaveandonEvent.behavioris gone.triggerOncebecameonceoroncePerDirection.scrollEvent,dimensionsandoffsetare gone.useWindowScroll/useContainerScrollbecameuseScrollPosition.useWindowDimensionsbecameuseViewportSize.
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.tsWhen 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 storybookThe latest Storybook build for react-atom-trigger is also available at
storybook.atomtrigger.dev.
CodeSandbox
Quick way to tweak it in the browser.
- Basic sentinel example
- Child mode threshold example
- Fixed header offset example
- Initial visible on load example
- Horizontal scroll container example
Development
pnpm install
pnpm lint
pnpm test
pnpm test:storybook
pnpm build
pnpm format:checkStorybook (Static Build)
Build:
pnpm build:sbOutput:
storybook-static/
This directory is used for deployment to storybook.atomtrigger.dev.
