Visibility Engine
createVisibilityEngine(pool?) — wraps a pooled IntersectionObserver with enter/leave edge detection and once support, without any framework-specific reactivity.
function createVisibilityEngine(pool?: ObserverPool): VisibilityEnginepool defaults to the shared observerPool singleton (see below) — pass your own new ObserverPool() to isolate observers from the rest of the app, which is mainly useful in tests.
Return value
observe(element, options, callback)
(element: Element, options: VisibilityObserveOptions, callback: (info: IntersectionInfo) => void) => () => void
Starts observing element. Returns an unsubscribe function.
destroy()
() => void
Runs every registered unsubscribe function and clears them.
VisibilityObserveOptions
| Field | Type | Default |
|---|---|---|
root | Element | Document | null | null |
rootMargin | string | '0px' |
threshold | number | number[] | 0 |
once | boolean | undefined — when true, observe() unobserves the element automatically after the first isIntersecting: true. |
onEnter | (info: IntersectionInfo) => void | — called when info.edge is 'enter-top' or 'enter-bottom'. |
onLeave | (info: IntersectionInfo) => void | — called when info.edge is 'leave-top' or 'leave-bottom'. |
IntersectionInfo
Passed to callback (and to onEnter/onLeave) on every change:
| Field | Type | Notes |
|---|---|---|
isIntersecting | boolean | |
intersectionRatio | number | |
boundingClientRect | DOMRectLike | |
edge | IntersectionEdge | undefined | 'enter-top' | 'enter-bottom' | 'leave-top' | 'leave-bottom' — only set when isIntersecting differs from the previous call. |
Example:
import { createVisibilityEngine } from '@macrulez/inview-core'
const visibility = createVisibilityEngine()
const unobserve = visibility.observe(
document.querySelector('#hero')!,
{ threshold: 0.5, once: true },
(info) => {
if (info.isIntersecting) console.log('half visible')
},
)Observer Pooling
ObserverPool / observerPool — pools native IntersectionObserver instances by (root, rootMargin, threshold), so many elements observed with the same options share one observer instead of each spawning its own. createVisibilityEngine() uses the shared observerPool singleton by default.
class ObserverPool {
observe(
element: Element,
options: ObserverPoolOptions,
callback: (entry: IntersectionObserverEntry) => void,
): () => void
}
const observerPool: ObserverPoolObserverPoolOptions is { root?, rootMargin?, threshold? } — the same three fields listed above, with the same defaults (null/'0px'/0), applied when a new native IntersectionObserver is actually constructed for a given key. The pool disconnects and drops a native observer once its last observed element unobserves. In an environment without IntersectionObserver at all, observe() returns a no-op unsubscribe rather than throwing.
The pool key compares a threshold array by value ([0, 0.25, 0.5].join(',')), not by reference — passing a fresh { threshold: [0, 0.5] } object literal on every single call still reuses the same native observer for elements sharing that value, no need to hoist the array to a shared constant yourself.
ObserverPool.stats()
() => ObserverPoolStat[]
Debug-only introspection into the pool — how many native IntersectionObserver instances are actually alive right now, and how many elements sit on each. Useful for confirming pooling is really collapsing observers as expected in a given app, instead of silently fragmenting because a fresh inline threshold array turned out to carry different values on every render than you assumed.
interface ObserverPoolStat {
key: string // the (root, rootMargin, threshold) pooling key
elementCount: number
}import { observerPool } from '@macrulez/inview-core'
observerPool.stats()
// [{ key: 'window|0px|0,0.5', elementCount: 12 }, { key: 'r|0px|0', elementCount: 3 }]Looking for the reactive version? See Element Visibility for Vue, or React Hooks for React — both wrap this same engine and share the same pool. Need to handle many elements — including ones that don't exist in the DOM yet — without calling a composable/hook per element? See Reveal Controller.