{"id":27284502,"url":"https://github.com/innrvoice/react-atom-trigger","last_synced_at":"2026-04-25T06:08:45.645Z","repository":{"id":40448937,"uuid":"471741607","full_name":"innrvoice/react-atom-trigger","owner":"innrvoice","description":"Simple alternative to react-waypoint","archived":false,"fork":false,"pushed_at":"2025-03-29T01:57:23.000Z","size":200,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-07-03T00:04:58.374Z","etag":null,"topics":["react","react-hooks","scrollspy","scrollview","typescript"],"latest_commit_sha":null,"homepage":"https://visiofutura.com/solving-scroll-into-view-problem-in-react-my-way-a8056a1bdc11","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/innrvoice.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2022-03-19T15:52:19.000Z","updated_at":"2025-03-29T01:56:40.000Z","dependencies_parsed_at":"2025-01-31T22:06:48.907Z","dependency_job_id":"b546d572-2c51-4863-9454-886dd179f1af","html_url":"https://github.com/innrvoice/react-atom-trigger","commit_stats":null,"previous_names":[],"tags_count":14,"template":false,"template_full_name":null,"purl":"pkg:github/innrvoice/react-atom-trigger","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/innrvoice%2Freact-atom-trigger","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/innrvoice%2Freact-atom-trigger/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/innrvoice%2Freact-atom-trigger/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/innrvoice%2Freact-atom-trigger/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/innrvoice","download_url":"https://codeload.github.com/innrvoice/react-atom-trigger/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/innrvoice%2Freact-atom-trigger/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":264578873,"owners_count":23631555,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["react","react-hooks","scrollspy","scrollview","typescript"],"created_at":"2025-04-11T18:58:36.812Z","updated_at":"2026-04-12T22:24:46.115Z","avatar_url":"https://github.com/innrvoice.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# react-atom-trigger\n\n[![codecov](https://codecov.io/gh/innrvoice/react-atom-trigger/branch/master/graph/badge.svg)](https://codecov.io/gh/innrvoice/react-atom-trigger)\n[![bundle size](https://codecov.io/github/innrvoice/react-atom-trigger/branch/master/graph/bundle/react-atom-trigger-esm/badge.svg)](https://app.codecov.io/github/innrvoice/react-atom-trigger/bundles/master/react-atom-trigger-esm)\n\n`react-atom-trigger` helps with the usual \"run some code when this thing enters or leaves view\" problem.\nIt is a lightweight React alternative to `react-waypoint`, written in TypeScript.\n\n## v2 is a breaking release\n\nIf you are coming from `v1.x`, please check [MIGRATION.md](./MIGRATION.md).\n\nIf you want to stay on the old API:\n\n```bash\n# pnpm\npnpm add react-atom-trigger@^1\n\n# npm\nnpm install react-atom-trigger@^1\n\n# yarn\nyarn add react-atom-trigger@^1\n```\n\n## Install\n\n```bash\n# pnpm\npnpm add react-atom-trigger\n\n# npm\nnpm install react-atom-trigger\n\n# yarn\nyarn add react-atom-trigger\n```\n\nThe published package does not enforce a specific Node.js engine.\nRuntime compatibility is determined by your React version, browser target and bundler setup.\n\nThe public React compatibility contract for `v2` is the published peer range: React `16.8` through\n`19.x`.\n\n## How it works\n\n`react-atom-trigger` uses a mixed approach.\n\n- Geometry is the real source of truth for `enter` and `leave`.\n- `IntersectionObserver` is only there to wake things up when the browser notices a nearby layout shift.\n- `rootMargin` logic is handled by the library itself, so it stays consistent and does not depend on native observer quirks.\n\nIn practice this means `AtomTrigger` reacts to:\n\n- scroll\n- window resize\n- root resize\n- sentinel resize\n- nearby layout shifts that move the observed element even if no scroll event happened\n\nThis is the main reason `v2` can support custom margin-aware behavior and still react to browser-driven layout changes.\n\n## Quick start\n\n```tsx\nimport React from 'react';\nimport { AtomTrigger } from 'react-atom-trigger';\n\nexport function Example() {\n  return (\n    \u003cAtomTrigger\n      onEnter={event =\u003e {\n        console.log('entered', event);\n      }}\n      onLeave={event =\u003e {\n        console.log('left', event);\n      }}\n      rootMargin=\"0px 0px 160px 0px\"\n      oncePerDirection\n    /\u003e\n  );\n}\n```\n\nIf you want an already-visible trigger to behave like a normal first `enter`, pass\n`fireOnInitialVisible`.\n\n```tsx\nimport React from 'react';\nimport { AtomTrigger } from 'react-atom-trigger';\n\nexport function RestoredScrollExample() {\n  return (\n    \u003cAtomTrigger\n      fireOnInitialVisible\n      onEnter={event =\u003e {\n        if (event.isInitial) {\n          console.log('started visible after load');\n          return;\n        }\n\n        console.log('entered from scrolling');\n      }}\n    /\u003e\n  );\n}\n```\n\n## Child mode\n\nIf you pass one top-level child, `AtomTrigger` observes that element directly instead of rendering its own sentinel.\n\n```tsx\nimport React from 'react';\nimport { AtomTrigger } from 'react-atom-trigger';\n\nexport function HeroTrigger() {\n  return (\n    \u003cAtomTrigger threshold={0.75} onEnter={() =\u003e console.log('hero is mostly visible')}\u003e\n      \u003csection style={{ minHeight: 240 }}\u003eHero content\u003c/section\u003e\n    \u003c/AtomTrigger\u003e\n  );\n}\n```\n\nThis is usually the better mode when `threshold` should depend on a real element size.\n\nIntrinsic elements such as `\u003cdiv\u003e` and `\u003csection\u003e` work automatically.\n\nIf you use a custom component, the ref that `AtomTrigger` passes down still has to reach a real DOM\nelement:\n\n- in React 19, the component can receive `ref` as a prop and pass it through\n- in React 18 and older, use `React.forwardRef`\n\nIf the ref never reaches a DOM node, child mode cannot observe anything.\n\nIf a custom child renders a placeholder first and only exposes its DOM node a moment later,\n`AtomTrigger` waits briefly before showing the missing-ref warning so normal async mount flows do\nnot get flagged too early.\n\n## API\n\n```ts\ninterface AtomTriggerProps {\n  onEnter?: (event: AtomTriggerEvent) =\u003e void;\n  onLeave?: (event: AtomTriggerEvent) =\u003e void;\n  onEvent?: (event: AtomTriggerEvent) =\u003e void;\n  children?: React.ReactNode;\n  once?: boolean;\n  oncePerDirection?: boolean;\n  fireOnInitialVisible?: boolean;\n  disabled?: boolean;\n  threshold?: number;\n  root?: Element | null;\n  rootRef?: React.RefObject\u003cElement | null\u003e;\n  rootMargin?: string | [number, number, number, number];\n  className?: string;\n}\n```\n\n### Props in short\n\n- `onEnter`, `onLeave`, `onEvent`: trigger callbacks with a rich event payload.\n- `children`: observe one real child element instead of the internal sentinel.\n- `once`: allow only the first transition overall.\n- `oncePerDirection`: allow one `enter` and one `leave`.\n- `fireOnInitialVisible`: emit an initial `enter` when observation starts and the trigger is already active.\n- `disabled`: stop observing without unmounting the component.\n- `threshold`: a number from `0` to `1`. It affects `enter`, not `leave`.\n- `root`: use a specific DOM element as the visible area.\n- `rootRef`: same idea as `root`, but better when the container is created in JSX. If both are passed, `rootRef` wins.\n- `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.\n- `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.\n- `className`: applies only to the internal sentinel.\n\n## Event payload\n\n```ts\ntype AtomTriggerEvent = {\n  type: 'enter' | 'leave';\n  isInitial: boolean;\n  entry: AtomTriggerEntry;\n  counts: {\n    entered: number;\n    left: number;\n  };\n  movementDirection: 'up' | 'down' | 'left' | 'right' | 'stationary' | 'unknown';\n  position: 'inside' | 'above' | 'below' | 'left' | 'right' | 'outside';\n  timestamp: number;\n};\n```\n\n```ts\ntype AtomTriggerEntry = {\n  target: Element;\n  rootBounds: DOMRectReadOnly | null;\n  boundingClientRect: DOMRectReadOnly;\n  intersectionRect: DOMRectReadOnly;\n  isIntersecting: boolean;\n  intersectionRatio: number;\n  source: 'geometry';\n};\n```\n\nThe payload is library-owned geometry data. It is not a native `IntersectionObserverEntry`.\n\n`isInitial` is `true` only for the synthetic first `enter` created by\n`fireOnInitialVisible`.\n\n## Hooks\n\nFor someone who wants everything out-of-the-box, `useScrollPosition` and `useViewportSize` are also available.\n\n```ts\nuseScrollPosition(options?: {\n  target?: Window | HTMLElement | React.RefObject\u003cHTMLElement | null\u003e;\n  passive?: boolean;\n  throttleMs?: number;\n  enabled?: boolean;\n}): { x: number; y: number }\n```\n\n```ts\nuseViewportSize(options?: {\n  passive?: boolean;\n  throttleMs?: number;\n  enabled?: boolean;\n}): { width: number; height: number }\n```\n\nBoth 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`.\n\nIf you pass `enabled={false}`, the hook pauses its listeners but keeps the latest value it already knows.\nIt does not fake a reset back to zero.\nWhen you enable it again, it reads from the source immediately and then continues updating as usual.\n\n## Notes\n\n- In sentinel mode, `threshold` is usually only interesting if your sentinel has real width or height. The default sentinel is almost point-like.\n- 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.\n- 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.\n- 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.\n- If you pass `root` or `rootRef` explicitly and it is not ready yet, observation pauses instead of falling back to the viewport.\n- `rootMargin` is handled by the library geometry logic. `IntersectionObserver` is only used as a nearby wake-up signal for layout shifts.\n\n## Migration from v1\n\nThe short version:\n\n1. `callback` became `onEnter`, `onLeave` and `onEvent`.\n2. `behavior` is gone.\n3. `triggerOnce` became `once` or `oncePerDirection`.\n4. `scrollEvent`, `dimensions` and `offset` are gone.\n5. `useWindowScroll` / `useContainerScroll` became `useScrollPosition`.\n6. `useWindowDimensions` became `useViewportSize`.\n\nFor the real upgrade notes and examples, see [MIGRATION.md](./MIGRATION.md).\n\n## Build output\n\nThis package is built with `tsdown`.\n\n```text\nlib/index.js\nlib/index.umd.js\nlib/index.d.ts\n```\n\nWhen the UMD bundle is loaded directly in the browser, the library is exposed as `window.reactAtomTrigger`.\n\n## Examples\n\n### Storybook\n\nStorybook is the easiest way to see how the component behaves.\n\n- `AtomTrigger Demo`: regular usage examples.\n- `Extended Demo`: a larger animated interaction demo that shows AtomTrigger driving scene changes,\n  event timing and more realistic scroll-based UI behavior.\n- `Internal Tests`: interaction stories used for local checks and Storybook tests.\n\nTo run Storybook locally:\n\n```bash\npnpm storybook\n```\n\nThe latest Storybook build for `react-atom-trigger` is also available at\n[storybook.atomtrigger.dev](https://storybook.atomtrigger.dev/).\n\n### CodeSandbox\n\nQuick way to tweak it in the browser.\n\n- [Basic sentinel example](https://codesandbox.io/p/sandbox/react-atom-trigger-v2-basic-example-9xrzmg)\n- [Child mode threshold example](https://codesandbox.io/p/sandbox/react-atom-trigger-v2-child-mode-threshold-qcpv28)\n- [Fixed header offset example](https://codesandbox.io/p/devbox/react-atom-trigger-v2-fixed-header-offset-62lmrv)\n- [Initial visible on load example](https://codesandbox.io/p/devbox/react-atom-trigger-v2-initial-visible-on-load-ncqjtf)\n- [Horizontal scroll container example](https://codesandbox.io/p/devbox/react-atom-trigger-v2-horizontal-scroll-container-hs33gq)\n\n## Development\n\n```bash\npnpm install\npnpm lint\npnpm test\npnpm test:coverage\npnpm test:storybook\npnpm build\npnpm format:check\n```\n\nCoverage note:\n\n- `pnpm test:coverage` is the official unit coverage signal used in CI and Codecov.\n- `pnpm test:storybook` is a separate browser regression gate and is not merged into the official coverage number.\n\n## Storybook (Static Build)\n\nBuild:\n\n```bash\npnpm build:sb\n```\n\nOutput:\n\n`storybook-static/`\n\nThis directory is used for deployment to `storybook.atomtrigger.dev`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Finnrvoice%2Freact-atom-trigger","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Finnrvoice%2Freact-atom-trigger","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Finnrvoice%2Freact-atom-trigger/lists"}