Skip to content

API

All functions are exported from prop-for-that.

import { configure, register, unregister, isRegistered, propsFor, unbind, reset, pause, resume } from 'prop-for-that'

Override the property prefixes or the global root element. Call this once, before any propsFor() calls.

function configure(opts: Partial<Config>): void
interface Config {
livePrefix: string // default: '--live-'
constPrefix: string // default: '--const-'
root: HTMLElement // default: document.documentElement (:root)
typed: boolean // default: false; register --live-* with @property
defaults?: Record<string, string | number> // typed initial values, by local name
liveHz?: number // default: unset (every frame); cap loop sampling+flush rate
}

Set typed: true to register written --live-* properties with @property, making them interpolatable and giving them a guaranteed initial value. Pass defaults (keyed by a source’s local name, e.g. 'pointer-x-ratio') to choose those initial values. See Typed properties. With the auto entry, <html data-props-typed> is the markup equivalent of typed: true.

Set liveHz to cap how often the shared loop samples and flushes — e.g. 30 coalesces writes to at most 30/sec. Fewer custom-property mutations means less style recalc and a calmer DevTools Styles panel, at the cost of update smoothness. It throttles the whole loop, so per-frame samplers like fps and scroll-velocity measure at this rate too.

Example

configure({
livePrefix: '--pft-live-',
constPrefix: '--pft-const-',
})

Add a custom or plugin source to the registry so it can be used by key via propsFor() or data-props-for.

function register(source: Source): void
interface Source {
key: SourceKey // unique identifier used in propsFor/data-props-for
scope: 'global' | 'element'
gate?: boolean // element sources are viewport-gated by default; set false to opt out (visibility does)
props?: Record<string, PropSpec> // optional @property typings per local name, applied when typed
start(ctx: SourceContext): Disposer
}
interface SourceContext {
target: HTMLElement
config: Config
write(localName: string, value: string | number, cadence?: 'live' | 'const'): void
}
type Disposer = () => void

Example

import { register, propsFor } from 'prop-for-that'
import { scrollVelocity } from 'prop-for-that/plugins'
register(scrollVelocity)
propsFor(['scroll-velocity'])

Remove a source from the registry by key, so it can no longer be attached. Already-active bindings of that key are unaffected; tear those down with their disposer or unbind().

function unregister(key: SourceKey): void

Example

import { unregister } from 'prop-for-that'
unregister('scroll-velocity')

Whether a source key is currently registered — either a core source or one added via register(). Handy for guarding a register() call or for tooling. (The auto entry uses it to decide whether a data-props-for key needs its plugin chunk loaded.)

function isRegistered(key: SourceKey): boolean

The single entry point for attaching sources. It’s global (writes to the configured root, :root by default) unless the first argument is a Node, a NodeList, or an array of elements, in which case the sources attach to that element (or to each element). Returns a disposer that tears down exactly what that call started and removes the custom properties it wrote.

function propsFor(keys: string[]): Disposer
function propsFor(target: Element | NodeList | Element[], keys: string[]): Disposer

Global (writes to :root):

import { register, propsFor } from 'prop-for-that'
import { pointer } from 'prop-for-that/plugins'
register(pointer) // pointer is an opt-in plugin; viewport is core
const dispose = propsFor(['viewport', 'pointer'])
// later, stop both sources and remove the properties they wrote
dispose()

Per element:

// an element's id is already a global in JS — no query selector needed
// (here, <input id="slider" type="range">)
const dispose = propsFor(slider, ['range'])
// later, clean up
dispose()

Across a NodeList or array (attaches to each element):

const dispose = propsFor(document.querySelectorAll('[data-reveal]'), ['visibility'])
// one disposer tears down every element this call touched
dispose()

Re-attaching a key that’s already active on the same target is a no-op for that key.


Detach specific source keys from an element, or all sources if keys is omitted.

function unbind(target: HTMLElement, keys?: string[]): void

Examples

// Remove just the 'range' source
unbind(slider, ['range'])
// Remove all sources from this element
unbind(slider)

The element is removed from the internal bindings map once all its sources are detached.


Tear down everything: every active binding (global and per element) plus the shared observers and listeners. Use it to fully clean up, for example between tests or before a hot reload.

function reset(): void

Example

import { reset } from 'prop-for-that'
reset()

Freeze and unfreeze the shared frame loop. While paused, no source samples and no writes flush, so every live value holds its current state — useful for inspecting them in DevTools without the per-frame churn. Bindings stay attached; resume() picks sampling back up and flushes anything queued meanwhile. Both are idempotent.

Backgrounded tabs are paused automatically (the loop freezes on document.hidden), so you don’t need to wire that up yourself — reach for pause()/resume() for your own freeze-on-demand cases.

function pause(): void
function resume(): void

Example

import { pause, resume } from 'prop-for-that'
// a "freeze" toggle — handy for inspecting live values in DevTools
let frozen = false
freezeButton.addEventListener('click', () => {
frozen = !frozen
frozen ? pause() : resume()
})