API
All functions are exported from prop-for-that.
import { configure, register, unregister, isRegistered, propsFor, unbind, reset, pause, resume } from 'prop-for-that'configure(opts)
Section titled “configure(opts)”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-',})register(source)
Section titled “register(source)”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 = () => voidExample
import { register, propsFor } from 'prop-for-that'import { scrollVelocity } from 'prop-for-that/plugins'
register(scrollVelocity)propsFor(['scroll-velocity'])unregister(key)
Section titled “unregister(key)”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): voidExample
import { unregister } from 'prop-for-that'
unregister('scroll-velocity')isRegistered(key)
Section titled “isRegistered(key)”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): booleanpropsFor(...)
Section titled “propsFor(...)”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[]): Disposerfunction propsFor(target: Element | NodeList | Element[], keys: string[]): DisposerGlobal (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 coreconst dispose = propsFor(['viewport', 'pointer'])
// later, stop both sources and remove the properties they wrotedispose()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 updispose()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 toucheddispose()Re-attaching a key that’s already active on the same target is a no-op for that key.
unbind(target, keys?)
Section titled “unbind(target, keys?)”Detach specific source keys from an element, or all sources if keys is omitted.
function unbind(target: HTMLElement, keys?: string[]): voidExamples
// Remove just the 'range' sourceunbind(slider, ['range'])
// Remove all sources from this elementunbind(slider)The element is removed from the internal bindings map once all its sources are detached.
reset()
Section titled “reset()”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(): voidExample
import { reset } from 'prop-for-that'
reset()pause() / resume()
Section titled “pause() / resume()”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(): voidfunction resume(): voidExample
import { pause, resume } from 'prop-for-that'
// a "freeze" toggle — handy for inspecting live values in DevToolslet frozen = falsefreezeButton.addEventListener('click', () => { frozen = !frozen frozen ? pause() : resume()})