Skip to content

Typed properties

By default, prop-for-that writes plain custom properties. They work everywhere, but to the engine they’re untyped (<*>): they can’t be interpolated (a transition or @keyframes on them snaps instead of tweening), and var(--live-x) is invalid until a value is written, which is why you reach for var(--live-x, fallback).

Opt in, and the library registers each --live-* it writes with @property (via CSS.registerProperty):

import { configure, register, propsFor } from 'prop-for-that'
import { pointer } from 'prop-for-that/plugins'
configure({ typed: true }) // before attaching any sources
register(pointer) // pointer is an opt-in plugin
propsFor(['pointer'])

Each property then gets:

  • a <number> type, so it interpolates: a transition or animation tweens it instead of snapping;
  • a guaranteed initial value of 0, so var(--live-x) resolves with no fallback.

Using the zero-config auto entry — binding through data-props-for instead of calling propsFor() — you can opt in straight from markup by adding data-props-typed to the root <html>:

<html data-props-typed>
<body>
<input type="range" data-props-for="range">
<script type="module">import 'prop-for-that/auto'</script>
</body>
</html>

Read once on load, it’s the exact equivalent of configure({ typed: true }): every --live-* the page writes is registered with @property. It’s a boolean@property is registered per name for the whole document, not per element, so typing is all-or-nothing and any attribute value is ignored (there’s no per-key subset, just as there isn’t in the JS API). For per-property initial values, reach for the JS defaults below.

Nothing animates unless you add a transition or @keyframes, so turning typed on doesn’t change how an existing stylesheet renders, except that var(--live-x) now resolves to 0 before a value arrives instead of being invalid. Smoothing a jumpy value is then one line:

.bar {
width: calc(var(--live-value-pct) * 100%);
transition: --live-value-pct 120ms ease-out; /* tweens because it's typed */
}

Set per-property defaults in the same configure call, keyed by a source’s local name. They become the @property initial value, so var(--live-…) resolves to them before any data arrives, overriding the source’s own default and the 0 fallback:

configure({
typed: true,
defaults: {
'pointer-x-ratio': 0.5, // centered until the pointer moves
'pointer-y-ratio': 0.5,
},
})

The value must be valid for the property’s syntax (a plain number for the default <number> typing). Defaults only apply when typed is on.

  • Opt in before attaching. Call configure({ typed: true }) before your first propsFor() — or set data-props-typed on <html> before auto runs — like the prefixes.
  • One-way door. @property registrations last the page’s lifetime (the spec has no unregister). Properties are still cleared from elements on teardown; only the type registration persists.
  • Inherited. Registrations use inherits: true, which the container-bound sources (range, field, media) rely on.
  • Safe to double up. If you, or another library, already declared @property --live-…, prop-for-that skips it rather than throwing.
  • Feature-detected. Where CSS.registerProperty is unavailable it is a no-op and properties stay untyped. Support: Chromium 85+, Safari 16.4+, Firefox 128+.

A custom or plugin source can declare per-property typings via props, applied only when typed is on. Anything you don’t declare defaults to <number> / 0 / inherited:

register({
key: 'tilt',
scope: 'element',
props: { angle: { syntax: '<angle>', initial: '0deg' } },
start(ctx) {
/* … ctx.write('angle', 12) … */
},
})