Skip to content

Plugins

Plugins cover state beyond the four core sources: pointer position, sensors, network, battery, media, and more. They live in one tree-shakeable entry, prop-for-that/plugins, and you register only the ones you use, so nothing you don’t import reaches your bundle.

Import the plugin, register() it once, then use it by key with propsFor(), exactly like a core source:

import { register, propsFor } from 'prop-for-that'
import { battery } from 'prop-for-that/plugins'
register(battery)
propsFor(document.documentElement, ['battery'])
.indicator {
width: calc(var(--live-battery-level) * 100%);
}

Register several at once with registerPlugins(). Pass specific sources, or no arguments to register everything:

import { registerPlugins, fps, clock } from 'prop-for-that/plugins'
registerPlugins(fps, clock) // just these two
registerPlugins() // or everything

Register, then attach with propsFor([...]); they write to :root.

ImportKeyProperties
pointerpointer--live-pointer-x, --live-pointer-y (px), --live-pointer-x-ratio, --live-pointer-y-ratio (0–1 across the viewport). High-frequency (a write per pointermove), so it’s an opt-in plugin rather than a core source.
scrollVelocityscroll-velocity--live-scroll-velocity (signed px/frame, decays to 0), --live-scroll-direction (1 / -1 / 0)
onlineonline--live-online (1 / 0)
pageFocusedpage-focused--live-page-focused (1 / 0) — the tab is frontmost and its window is focused; goes 0 when the user switches to another tab, window, or app. Seeded from document.hasFocus(), driven by window focus/blur.
pageVisiblepage-visible--live-page-visible (1 / 0) — the tab is in the foreground vs hidden (backgrounded, minimized, switched away). Distinct from page-focused: a visible tab can still be unfocused. Use it to pause video/polling/expensive effects when truly hidden. Seeded from document.visibilityState, driven by visibilitychange.
navTypenav-type--const-nav-type — how the user arrived, written once as a string: navigate, reload, back_forward (history traversal / bfcache), or prerender, from performance.getEntriesByType('navigation')[0].type. Pairs with style queries — @container style(--const-nav-type: reload) { … }. Rides the writer, so it lands the frame after bind; read the same value in a <head> script if you need it before first paint.
networknetwork--live-net-downlink, --live-net-rtt, --live-net-save-data (1 / 0), --live-net-type (slow-2g=1 … 4g=4, else 0)
batterybattery--live-battery-level (0–1), --live-battery-charging (1 / 0)
clockclock--live-now (epoch seconds), --live-hours, --live-minutes, --live-seconds
fpsfps--live-fps (rounded)
visualViewportvisual-viewport--live-vvp-scale, --live-vvp-offset-top, --live-vvp-height
orientationorientation--live-orient-alpha, --live-orient-beta, --live-orient-gamma (degrees)
motionmotion--live-accel-x, --live-accel-y, --live-accel-z (m/s², gravity included)
geogeo--live-geo-lat, --live-geo-lng, --live-geo-accuracy (metres)
cpuPressurecpu-pressure--live-cpu-pressure (CPU Compute Pressure as an ordered tier: nominal=0, fair=1, serious=2, critical=3)

Register, then attach with propsFor(el, [...]); they write to the bound element. In auto mode you don’t register at all — a data-props-for attribute loads the plugin on demand:

<script type="module" src="https://unpkg.com/prop-for-that/dist/auto.js"></script>
<input data-props-for="field"> <!-- the field plugin loads itself the first time it's seen -->
ImportKeyProperties
pointerLocalpointer-local--live-local-pointer-x-ratio, --live-local-pointer-y-ratio (0–1 within the element’s box), --live-local-pointer-inside (1 / 0)
mediamedia--live-current-time, --live-duration, --live-progress (0–1), --live-paused (1 / 0), --live-volume (0–1), for <video> / <audio>
fieldfield--live-length (value length), --live-empty (1 / 0), --live-valid (1 / 0), for form fields. Plus the per-reason validity flags :invalid can’t distinguish (each 1 / 0): --live-value-missing, --live-type-mismatch, --live-pattern-mismatch, --live-too-long, --live-too-short, --live-range-underflow, --live-range-overflow, --live-step-mismatch, --live-bad-input, --live-custom-error. And, when the field has a maxlength: --live-remaining (chars left), --live-fill-pct (01) for a pure-CSS counter (neither written without a maxlength — keep a var() fallback) — see the Field demo
selectselecta <select>’s state as numbers CSS can’t reach: --live-index (selectedIndex, -1 if none), --live-option-count, --live-index-pct (01), --live-value-num (Number(value), only when the value is numeric), --live-selected-count and --live-selected-pct (01) — the multi-select tally :checked can’t count. Drive a sliding segmented indicator off --live-index, or layout off --live-value-num. Bind the <select> or a container — see the Select demo
colorInputcolor-input--live-color — the colour chosen in an <input type="color"> (CSS can’t read it), as one sRGB hex string; pull channels out with relative colour syntax / color-mix(). Typed mode registers it <color> so it interpolates between picks. Bind the input or a container — see the Color input demo
fieldStatefield-statethe interaction history form libraries track but CSS can’t (all 1 / 0): --live-dirty / --live-pristine (edited at all — latches), --live-touched / --live-untouched (blurred at least once — latches), --live-changed (current value differs from mount — un-latches), --live-submitted (owning <form> submitted). Bind a single field, or a <form> for the aggregate over all its fields. Pairs with field (validity) and :focus (focus) — see the Field state demo
formStateform-stateform-level validity & completion CSS can’t count: --live-field-count, --live-valid-count, --live-invalid-count, --live-all-valid (1 / 0 — the submit gate), --live-completion (01, valid required ÷ required). Bind a <form> / wrapper — see the Form state demo
imgimg--live-natural-w, --live-natural-h (intrinsic pixel size), --live-loaded (1 / 0), --live-broken (1 / 0), for <img>
imgColorimg-colora palette from an <img> (CSS can’t read pixels), each swatch a single sRGB hex colour (no channel props — extract with relative colour syntax / color-mix()): --live-img (dominant), --live-img-accent (most vibrant), --live-img-dark (darkest non-black), --live-img-light (lightest non-white), --live-img-avg (mean), plus --live-img-temp (−1 cool … +1 warm). All from one 16×16 createImageBitmap + OffscreenCanvas pass; cross-origin images need crossorigin + CORS or it no-ops — see the Image color demo
videoColorvideo-colorlive colours of a playing <video>, each a single sRGB hex colour: --live-video (dominant) + --live-video-accent (the most vibrant; reuses the dominant for a grayscale frame). Both read 16×16 on requestVideoFrameCallback (so it stops sampling when the video is paused, offscreen, or backgrounded), throttled to ~4 Hz; cross-origin video needs crossorigin + CORS or it no-ops — see the Video color demo

Every plugin feature-detects the API it needs and quietly no-ops (returns an empty disposer) when it’s unavailable, so registering one is always safe.

orientation, motion, and geo are additionally permission-gated: on some platforms (notably iOS) they require a user-gesture permission grant before any values arrive.

cpuPressure is Chromium-only today and needs a secure context plus the compute-pressure Permissions Policy (allowed for same-origin by default). It seeds --live-cpu-pressure: 0 (nominal) where supported and writes nothing where it isn’t, so var(--live-cpu-pressure, 0) is a safe read everywhere.

imgColor and videoColor read pixels through a canvas, so a cross-origin image or video needs crossorigin="anonymous" plus permissive CORS headers; otherwise the canvas is tainted and the plugin writes nothing. Same-origin, data:, and blob: sources work without ceremony. Keep a var() fallback either way.

Not yet implemented, tracked for a later release: light (ambient light sensor), audio (Web-Audio analyser bins), caret (selection/caret position), and children (child count via MutationObserver).

Deliberately left out, because CSS or the platform already does them better:

  • :hover, :focus-within, :active: CSS pseudo-classes.
  • [open], :modal, :checked: HTML/CSS selectors.
  • prefers-*, media queries, container queries: CSS at-rules.
  • env() safe-area insets: already in CSS.
  • scroll position / progress: use native scroll-driven animations (animation-timeline: scroll() / view()), which run on the compositor. Scroll velocity and direction, which timelines can’t express, are the scroll-velocity plugin.