https://github.com/innrvoice/react-atom-trigger
Simple alternative to react-waypoint
https://github.com/innrvoice/react-atom-trigger
react react-hooks scrollspy scrollview typescript
Last synced: 2 months ago
JSON representation
Simple alternative to react-waypoint
- Host: GitHub
- URL: https://github.com/innrvoice/react-atom-trigger
- Owner: innrvoice
- License: mit
- Created: 2022-03-19T15:52:19.000Z (over 4 years ago)
- Default Branch: master
- Last Pushed: 2025-03-29T01:57:23.000Z (over 1 year ago)
- Last Synced: 2025-07-03T00:04:58.374Z (about 1 year ago)
- Topics: react, react-hooks, scrollspy, scrollview, typescript
- Language: TypeScript
- Homepage: https://visiofutura.com/solving-scroll-into-view-problem-in-react-my-way-a8056a1bdc11
- Size: 195 KB
- Stars: 3
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# react-atom-trigger
[](https://codecov.io/gh/innrvoice/react-atom-trigger)
[](https://app.codecov.io/github/innrvoice/react-atom-trigger/bundles/master/react-atom-trigger-esm)
`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](./MIGRATION.md).
If you want to stay on the old API:
```bash
# pnpm
pnpm add react-atom-trigger@^1
# npm
npm install react-atom-trigger@^1
# yarn
yarn add react-atom-trigger@^1
```
## Install
```bash
# pnpm
pnpm add react-atom-trigger
# npm
npm install react-atom-trigger
# yarn
yarn add react-atom-trigger
```
The published package does not enforce a specific Node.js engine.
Runtime compatibility is determined by your React version, browser target and bundler setup.
The public React compatibility contract for `v2` is the published peer range: React `16.8` through
`19.x`.
## 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 nearby 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
- nearby 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
```tsx
import React from 'react';
import { AtomTrigger } from 'react-atom-trigger';
export function Example() {
return (
{
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`.
```tsx
import React from 'react';
import { AtomTrigger } from 'react-atom-trigger';
export function RestoredScrollExample() {
return (
{
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.
```tsx
import React from 'react';
import { AtomTrigger } from 'react-atom-trigger';
export function HeroTrigger() {
return (
console.log('hero is mostly visible')}>
Hero content
);
}
```
This is usually the better mode when `threshold` should depend on a real element size.
Intrinsic elements such as `
` and `` 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.
If a custom child renders a placeholder first and only exposes its DOM node a moment later,
`AtomTrigger` waits briefly before showing the missing-ref warning so normal async mount flows do
not get flagged too early.
## API
```ts
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;
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.
- `root` / `rootRef`: if you pass one explicitly but it is still `null`, observation pauses until that real root exists. It does not silently fall back to the viewport.
- `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
```ts
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;
};
```
```ts
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.
```ts
useScrollPosition(options?: {
target?: Window | HTMLElement | React.RefObject;
passive?: boolean;
throttleMs?: number;
enabled?: boolean;
}): { x: number; y: number }
```
```ts
useViewportSize(options?: {
passive?: boolean;
throttleMs?: number;
enabled?: boolean;
}): { width: number; height: number }
```
Both hooks are SSR-safe and hydration-safe across the supported React range. During hydration, the first client render matches the server snapshot and then refreshes from the live source, including the compat path used when React does not expose `useSyncExternalStore`. Default throttling is `16ms`.
If you pass `enabled={false}`, the hook pauses its listeners but keeps the latest value it already knows.
It does not fake a reset back to zero.
When you enable it again, it reads from the source immediately and then continues updating as usual.
## Notes
- In sentinel mode, `threshold` is usually only interesting if your sentinel has real width or height. The default sentinel is almost point-like.
- The internal sentinel intentionally uses a non-block display so it behaves like a point-like marker instead of stretching into a full-width placeholder.
- 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.
- If you pass `root` or `rootRef` explicitly and it is not ready yet, observation pauses instead of falling back to the viewport.
- `rootMargin` is handled by the library geometry logic. `IntersectionObserver` is only used as a nearby 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](./MIGRATION.md).
## Build output
This package is built with `tsdown`.
```text
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:
```bash
pnpm storybook
```
The latest Storybook build for `react-atom-trigger` is also available at
[storybook.atomtrigger.dev](https://storybook.atomtrigger.dev/).
### CodeSandbox
Quick way to tweak it in the browser.
- [Basic sentinel example](https://codesandbox.io/p/sandbox/react-atom-trigger-v2-basic-example-9xrzmg)
- [Child mode threshold example](https://codesandbox.io/p/sandbox/react-atom-trigger-v2-child-mode-threshold-qcpv28)
- [Fixed header offset example](https://codesandbox.io/p/devbox/react-atom-trigger-v2-fixed-header-offset-62lmrv)
- [Initial visible on load example](https://codesandbox.io/p/devbox/react-atom-trigger-v2-initial-visible-on-load-ncqjtf)
- [Horizontal scroll container example](https://codesandbox.io/p/devbox/react-atom-trigger-v2-horizontal-scroll-container-hs33gq)
## Development
```bash
pnpm install
pnpm lint
pnpm test
pnpm test:coverage
pnpm test:storybook
pnpm build
pnpm format:check
```
Coverage note:
- `pnpm test:coverage` is the official unit coverage signal used in CI and Codecov.
- `pnpm test:storybook` is a separate browser regression gate and is not merged into the official coverage number.
## Storybook (Static Build)
Build:
```bash
pnpm build:sb
```
Output:
`storybook-static/`
This directory is used for deployment to `storybook.atomtrigger.dev`.