# prop-for-that
> Expose runtime state that JavaScript knows but CSS can't see, as batched, diffed
> CSS custom properties. CSS then computes and reacts to it via `var()` (continuous
> values) or `@container style()` (discrete values), with no JS in the animation path.
prop-for-that reads browser/runtime state (pointer, viewport, element size,
visibility, slider values, network, battery, sensors, and more) and writes it into
`--live-*` (reactive) and `--const-*` (write-once) custom properties. A single
`requestAnimationFrame` writer coalesces all updates and only calls `setProperty`
when a value actually changed. Zero runtime dependencies, tree-shakeable, TypeScript.
## Entry points
- `prop-for-that`: the core API (`configure`, `register`, `unregister`, `propsFor`, `unbind`, `reset`).
- `prop-for-that/auto`: side-effecting and fully declarative. Attaches **nothing** by default — it scans the DOM for `[data-props-for="key1 key2"]` and binds exactly those sources (globals included, declared on `` → writes to `:root`), watching the DOM with a `MutationObserver`. Plugin sources are **loaded on demand**: the first time a key needs one, its chunk is dynamically imported and registered, then the binding attaches (no `registerPlugins()`, no separate plugins import). Because it lazy-loads via dynamic `import()`, use it as a module — `
…
```
Imperative:
```js
import { propsFor } from 'prop-for-that'
propsFor(['viewport']) // core global → :root
propsFor(el, ['size', 'visibility']) // writes element vars to el
```
A plugin (pointer is one now — register before use; `auto` does this for you):
```js
import { register, propsFor } from 'prop-for-that'
import { pointer, battery } from 'prop-for-that/plugins'
register(pointer)
propsFor(['pointer']) // writes pointer vars to :root
register(battery); propsFor(['battery'])
```
Container-bound range (so siblings can read the value):
```js
// the source finds the inner but writes the vars on the wrapper
propsFor(document.querySelector('.meter'), ['range'])
```
Continuous reaction with `var()`:
```css
.card { transform: rotateY(calc((var(--live-pointer-x-ratio) - 0.5) * 16deg)); }
```
Discrete reaction with a style query:
```css
@container style(--live-online: 0) { .banner { display: block; } }
@container style(--live-value: 100) { .badge { color: var(--accent); } }
```
## Gotchas (read before writing CSS)
- **Custom properties inherit downward only.** An `element`-scoped source writes its vars on the bound element. To let *sibling* elements react, bind a common ancestor. `range` and `field` help here: bind them to a container and they find the inner ``, writing the vars on the container.
- **Pick the right consumer.** Continuous numbers go through `var()` + `calc()`. Discrete/boolean/integer values use `@container style()`, which matches on **equality only** (range comparisons like `style(--x < 5)` are not stable yet). Emit booleans/tiers when you need thresholds.
- **`visibility` is binary + latched, not a ratio.** It writes `--live-visible` and `--const-has-entered`. There is no continuous visible-ratio and no scroll-position source: use native `animation-timeline: scroll()` / `view()` for continuous scroll-driven effects.
- **Values are unitless.** Multiply by `1px`, `1deg`, `100%`, etc. in CSS.
- **Configure prefixes before attaching.** `configure({ livePrefix, constPrefix })` must run before any `propsFor()`.
- **Typed properties are opt-in.** `configure({ typed: true })` (before attaching) registers each written `--live-*` with `@property` so it interpolates (consumers add `transition`/`@keyframes`) and resolves to `0` before a value arrives. With the `auto` entry, `` is the markup equivalent (boolean, read once on load; typing is global per `@property` name, so no per-key subset). It is a one-way door (no unregister), uses `inherits: true`, is feature-detected, and skips names already registered. Sources can declare per-property `props: { name: { syntax, initial, inherits } }` (default `` / `0` / inherited); consumers can set initials with `configure({ typed: true, defaults: { 'pointer-x-ratio': 0.5 } })` (keyed by local name).
- **Permission-gated plugins** (`orientation`, `motion`, `geo`) and unsupported APIs feature-detect and no-op; registering them is always safe. On iOS some need a user-gesture permission grant before values arrive. `cpuPressure` is Chromium-only and needs a secure context + the `compute-pressure` Permissions Policy; it seeds `--live-cpu-pressure: 0` where supported, writes nothing where not.
- **Global writes land in an adopted stylesheet, not `` inline style.** Root `--live-*` go into one constructable, adopted `:root {}` rule (element-scoped writes stay inline; the `head` entry still writes `--const-*` inline for first paint). Keeps the DevTools Styles panel usable under per-frame churn; `var()`/`calc()` consumption is identical (still inherits). Reading `documentElement.style` directly won't show them — read computed style. Falls back to inline where constructable stylesheets are unsupported.
- **Element sources pause off screen.** Under both the imperative API and `auto`, an `element`-scoped source runs only while its element intersects the viewport (via the shared `IntersectionObserver`); off screen its work (listeners/observers/timers) is torn down and its last-written values freeze in place, resuming + re-seeding on re-entry. `global` sources and `:root` bindings are never gated. Opt a source out with `gate: false` (as `visibility` does, since it must keep observing to *report* visibility).
- **Freeze or throttle the loop.** The whole loop also freezes automatically while the tab is hidden (`document.hidden`). `pause()` / `resume()` stop and restart sampling+flushing (values hold steady; bindings stay attached). `configure({ liveHz: 30 })` caps the rate to reduce mutations/recalc, throttling samplers (`fps`, `scroll-velocity`) too.
## Links
- [Documentation](https://prop-for-that.netlify.app/docsite/): full guides, concepts, and API.
- [Plugin & source reference](https://prop-for-that.netlify.app/docsite/reference/plugins/): every source, key, and property.
- [Live demos](https://prop-for-that.netlify.app/): interactive examples of each source.
- [Under consideration](https://prop-for-that.netlify.app/docsite/considered/): deferred/proposed sources and voting.
- [Repository](https://github.com/argyleink/prop-for-that): source, issues, changelog, contributing.