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
randomSeed?: number // default: unset (Math.random()); seed the `random` plugin
}

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 randomSeed to make the random plugin deterministic: each element’s rolls are derived from the seed plus its position in the DOM instead of Math.random(), so the same markup renders the same “random” layout on every load. It’s read when a source starts, so it can differ between two propsFor() calls. With the auto entry, <html data-props-seed="42"> is the markup equivalent (a non-numeric value is ignored, with a warning).

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[], opts?: PropsForOptions): Disposer
interface PropsForOptions {
to?: Element | string // write the properties on this ancestor instead
}

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()

Hoisted to an ancestor (opts.to):

// observe the <img>, but write --live-loaded / --live-natural-w on the <figure>
const dispose = propsFor(img, ['img'], { to: 'figure' })
// an explicit element works too
propsFor(img, ['size'], { to: img.parentElement })

Custom properties inherit downward only, and @container style() matches against an ancestor — so an element can neither share nor style-query the values written on itself. to observes the bound element but writes its properties on an ancestor, which is what lets a <figcaption> (or a skeleton overlay, or the <img> itself) react. It takes a selector, resolved once at bind time with el.closest(), or an element. The markup equivalent is data-props-to. See Style queries.

Three things to know:

  • No match warns and degrades. If the selector matches no ancestor, the properties are written on the element itself (a console.warn says so) — the effect degrades rather than disappearing.
  • Globals ignore it. to on a global-scoped source warns and is ignored; globals already write to a shared target.
  • One binding per key per target. Two elements hoisting the same key onto one ancestor overwrite each other’s values, and disposing either removes the property. It warns in that case; give each hoist its own target.

Re-attaching a key that’s already active on the same target is a no-op for that key — including a re-attach with a different to. Change the target by tearing the binding down first (unbind(el, [key])), then binding again.


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()
})