Install
# pnpm
pnpm add react-atom-trigger
# npm
npm install react-atom-trigger
# yarn
yarn add react-atom-triggerThe published package does not enforce a specific Node.js engine.
The public React compatibility contract for v2 is the published peer range: 16.8 through 19.x.
Get started
Most of the time you just render AtomTrigger and handle enter and leave.
A common setup is rootMargin for trigger zone offsets and oncePerDirection when you want one enter and one leave.
<AtomTrigger
onEnter={event => {
console.log('entered', event);
}}
onLeave={event => {
console.log('left', event);
}}
rootMargin="-100px 0px 0px 0px"
oncePerDirection
/>It's easier to understand when you see it working.
Explore more examples on CodeSandbox:
Browse the Storybook
See the full interactive demo set in Storybook.
What changed in v2
In v1 you had to pass scroll position and container dimensions yourself.
In v2 AtomTrigger handles observation internally. It listens to scroll, resize and layout shifts and samples geometry on its own.
So most setups became just “render it and pass callbacks”.
- No external scroll plumbing
- Works with viewport or custom containers
- Handles fixed headers and offsets
- Can observe a real child element when threshold matters
- Emits richer event data
How it works
AtomTrigger uses a mixed approach.
Geometry is the source of truth for enter and leave.
IntersectionObserver wakes things up when the browser notices nearby layout changes, geometry decides what actually entered or left.
rootMargin is handled by the library itself, so the behavior stays consistent instead of depending on native observer quirks.
Because of that it reacts not only to scroll, but also to window resize, root resize, sentinel resize and layout shifts.
That is the main reason v2 can support custom margin-aware behavior and still react to browser-driven layout changes.
- reacts to scroll
- reacts to window resize
- reacts to root resize
- reacts to sentinel resize
- reacts to layout shifts that move the observed element even if no scroll event happened
API
The main props look like this.
The event payload is library-owned geometry data. It is not a native IntersectionObserverEntry.
fireOnInitialVisible lets you opt into an initial enter, and the payload exposes isInitial so you can treat restored visibility differently from scroll-driven transitions.
If root or rootRef is passed explicitly but is not ready yet, observation pauses until that real root exists. It does not silently fall back to the viewport.
In child mode, plain elements like div or section just work. If you use a custom component, the ref still has to make it down to a real DOM element. In React 19 that can be a ref prop, and in React 18 or older you usually do it with React.forwardRef.
v2.1 no longer exports scroll or viewport helper hooks. AtomTrigger observes internally, so use your app's own hooks when you need those values directly.
AtomTriggerProps
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;
}AtomTriggerEvent
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;
}Props in short
- onEnter, onLeave, onEventtrigger callbacks with a rich event payload.
- childrenobserve one real child element instead of the internal sentinel. Custom components need the received ref to reach a DOM node.
- onceallow only the first transition overall.
- oncePerDirectionallow one enter and one leave.
- fireOnInitialVisibleemit an initial enter when observation starts and the trigger is already active.
- disabledstop observing without unmounting the component.
- thresholda number from 0 to 1. It affects enter, not leave.
- rootuse a specific DOM element as the visible area. If it is explicitly passed as null, observation pauses instead of falling back to the viewport.
- rootRefsame idea as root, but better when the container is created in JSX. If both are passed, rootRef wins, and an unresolved ref pauses observation.
- rootMarginexpand or shrink the effective root. String values use IntersectionObserver-style syntax, and arrays are [top, right, bottom, left] pixels.
- classNameapplies only to the internal sentinel.
Upgrading from v1
This is a breaking release.
This is not just a prop rename release.
Please re-check the behavior in the real UI after upgrading, especially if you use threshold, rootMargin, custom scroll containers, or any code that was sensitive to the exact timing of the old callback.
The new version samples geometry internally, so some edge timing can feel a bit different near boundaries.
The short version
- callback became onEnter, onLeave, and onEvent.
- behavior is gone.
- triggerOnce became once or oncePerDirection.
- scrollEvent, dimensions, and offset are gone.
- legacy helper hooks are no longer exported in v2.1.
- use your app's own scroll or viewport hooks if needed.
For the real upgrade notes and examples, see MIGRATION.md on GitHub.