Skip to content

Scroll Engine

createScrollEngine(target?, options?) — a framework-agnostic scroll engine for the window or a scrollable element. Batches DOM scroll events into one shared requestAnimationFrame loop and notifies subscribers only when the derived state actually changes.

ts
function createScrollEngine(
  target?: Window | HTMLElement,
  options?: ScrollEngineOptions,
): ScrollEngine

target defaults to window on the client. When there's no window (server-side rendering) or no target at all, this returns a no-op engine with a static, zero-value state instead of throwing.

Options

idleTimeout

number · default: 150

How many milliseconds of scroll inactivity before isScrolling flips back to false (and velocity resets to 0).

Return value

getState()

() => ScrollState

Reads the current state synchronously, without subscribing.

subscribe(callback)

(callback: (state: ScrollState) => void) => () => void

Calls callback immediately with the current state, then again on every state change. Returns an unsubscribe function.

scrollTo(position, options?)

(position: number | { x?: number; y?: number }, options?: ScrollToOptions) => void

Scrolls the target. A plain number sets the vertical offset (top). options.behavior defaults to 'smooth', unless prefersReducedMotion() is true, in which case it falls back to 'auto'.

destroy()

() => void

Removes the scroll listener, clears the idle timer, unsubscribes from the shared rAF loop, and clears all subscribers.

ScrollState

The state object passed to subscribe() and returned by getState():

FieldTypeNotes
x, ynumberCurrent scroll offset.
direction'up' | 'down' | 'left' | 'right' | nullDerived from the last scroll delta — only changes on a nonzero delta.
progressnumber0..1, based on the vertical scroll range if the target is vertically scrollable, otherwise the horizontal range, otherwise 0.
velocitynumberPixels per frame, computed from the distance traveled between two rAF ticks.
isScrollingbooleantrue while scrolling, back to false once idleTimeout ms have passed with no scroll.

Example:

ts
import { createScrollEngine } from '@macrulez/inview-core'

const scroll = createScrollEngine(window, { idleTimeout: 200 })

const unsubscribe = scroll.subscribe((state) => {
  document.title = `${Math.round(state.progress * 100)}% scrolled`
})

// later
scroll.scrollTo({ y: 0 })
unsubscribe()
scroll.destroy()

Looking for the reactive version? See Reactive Scroll State for Vue, or React Hooks for React — both wrap this same engine.