# PositionObserver [![Coverage Status](https://coveralls.io/repos/github/thednp/position-observer/badge.svg)](https://coveralls.io/github/thednp/position-observer) [![ci](https://github.com/thednp/position-observer/actions/workflows/ci.yml/badge.svg)](https://github.com/thednp/position-observer/actions/workflows/ci.yml) [![NPM Version](https://img.shields.io/npm/v/@thednp/position-observer.svg)](https://www.npmjs.com/package/@thednp/position-observer) The **PositionObserver** is a lightweight utility that replaces traditional `resize` and `scroll` event listeners. Built on the [IntersectionObserver API](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver), it provides a way to asynchronously observe changes in the position of a target element with an ancestor element or with a top-level `document`'s viewport. ## Installation ```bash npm i @thednp/position-observer ``` ```bash bun add @thednp/position-observer ``` ```bash pnpm add @thednp/position-observer ``` ```bash deno add npm:@thednp/position-observer@latest ``` ## Usage ```ts import PositionObserver from '@thednp/position-observer'; // Find a target element const myTarget = document.getElementById('myElement'); // Define a callback const callback = (entries: IntersectionObserverEntry[], currentObserver: PositionObserver) => { // Access the observer inside your callback // console.log(currentObserver); entries.forEach((entry) => { if (entry.isIntersecting/* and your own conditions apply */) { // Handle position changes console.log(entry.boundingClientRect); } }) }; // Set options const options = { root: document.getElementById('myModal'), // Defaults to document.documentElement rootMargin: '0px', // Margin around the root, this applies to IntersectionObserver only threshold: 0, // Trigger when any part of the target is visible, this applies to IntersectionObserver only }; // Create the observer const observer = new PositionObserver(callback, options); // Start observing observer.observe(myTarget); // Example callback entries [{ target: , boundingClientRect: DOMRectReadOnly, intersectionRatio: number, isIntersecting: boolean, // ... other IntersectionObserverEntry properties }] // Get an entry observer.getEntry(myTarget); // Stop observing a target observer.unobserve(myTarget); // Resume observing observer.observe(myTarget); // Stop all observation observer.disconnect(); ``` ## Instance Options | Option | Type | Description | |--------| -----|-------------| | `root` | `Element` \| `undefined` | The element used as the viewport for checking target visibility. Defaults to `document.documentElement`.| | `rootMargin` | `string` \| `undefined` | Margin around the root of the `IntersectionObserver`. Uses same format as CSS margins (e.g., "10px 20px"). | | `threshold` | `number` \| `number[]` \| `undefined` | Percentage of the target's visibility required to trigger the `IntersectionObserver` callback. | ### root The **PositionObserver** `instance.root` identifies the `Element` whose bounds are treated as the bounding box of the viewport for the element which is the observer's target. Since we're observing for its width and height changes, this root can only be an instance of `Element`, so `Document` cannot be the root of your `PositionObserver` instance. The **IntersectionObserver** `instance.root` is always the default, which is `Document`. The two observers really care for different things: one cares about intersection the other cares about position, which is why the two observers cannot use the same root. When observing targets from a **scrollable** parent element, that parent must be set as root. The same applies to embeddings and `IFrame`s. See the [ScrollSpy](https://github.com/thednp/bootstrap.native/blob/master/src/components/scrollspy.ts) example for implementation details. The two initialization options specifically for the `IntersectionObserver` are `rootMargin` and `threshold`; they control the behavior of `PositionObserver` in the sense that when conditions are met for intersection, the `PositionObserver` entries are updated or skipped. ## How it Works * **Initialization**: Requires a valid callback function, or it throws an Error. * **Target Validation**: The `observe()` method requires a valid `Element`, or it throws an Error. Targets not attached to the DOM are ignored. * **Observation**: Tracks changes in the target's top or left position relative to the root, as well as the root's `clientWidth` and `clientHeight`. * **Intersection Checks**: Uses `IntersectionObserver` with the `document` as the root to determine `isIntersecting`. The `rootMargin` and `threshold` options apply to these checks. ## Notes * **Performance**: Use `entry.boundingClientRect` from `observer.getEntry(target)` to avoid redundant `getBoundingClientRect()` calls. * **Async Design**: Leverages `requestAnimationFrame` and `IntersectionObserver` for efficient, asynchronous operation. Consider wrapping callbacks in `requestAnimationFrame` for synchronization and to eliminate any potential [observation errors](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver#observation_errors). * **Visibility**: Targets must be visible (no `display: none` or `visibility: hidden`) for actual accurate bounding box measurements. * **Cleanup**: Call unobserve() or disconnect() when observation is no longer needed to free resources. * **ResizeObserver Alternative**: Filter callbacks on `entry.boundingClientRect.width` or height changes to mimic `ResizeObserver`. * **Scroll Optimization**: For scroll-specific changes, filter callbacks on `entry.boundingClientRect.top` or `left`. * **IntersectionObserver Root**: The underlying `IntersectionObserver` uses the `document` as its root, while `the PositionObserver`'s root option defines the reference `Element` for position tracking. * **IntersectionObserverEntry Spread**: This is an interface instance and cannot be [spread](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax). * **RootMargin and Threshold**: These options have no impact in `all` mode, as non-intersecting targets are still processed. They are however relevant in `intersecting` or `update` modes for defining visibility conditions. ## Special Thanks * [Bart Spaans](https://github.com/spaansba) for his awesome contributions. ## License The **PositionObserver** is released under the [MIT license](https://github.com/thednp/position-observer/blob/master/LICENSE).