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.
Using a plugin
Section titled “Using a plugin”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 tworegisterPlugins() // or everythingGlobal plugins
Section titled “Global plugins”Register, then attach with propsFor([...]); they write to :root.
| Import | Key | Properties |
|---|---|---|
pointer | pointer | --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. |
scrollVelocity | scroll-velocity | --live-scroll-velocity (signed px/frame, decays to 0), --live-scroll-direction (1 / -1 / 0) |
online | online | --live-online (1 / 0) |
pageFocused | page-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. |
pageVisible | page-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. |
navType | nav-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. |
network | network | --live-net-downlink, --live-net-rtt, --live-net-save-data (1 / 0), --live-net-type (slow-2g=1 … 4g=4, else 0) |
battery | battery | --live-battery-level (0–1), --live-battery-charging (1 / 0) |
clock | clock | --live-now (epoch seconds), --live-hours, --live-minutes, --live-seconds |
fps | fps | --live-fps (rounded) |
visualViewport | visual-viewport | --live-vvp-scale, --live-vvp-offset-top, --live-vvp-height |
orientation | orientation | --live-orient-alpha, --live-orient-beta, --live-orient-gamma (degrees) |
motion | motion | --live-accel-x, --live-accel-y, --live-accel-z (m/s², gravity included) |
geo | geo | --live-geo-lat, --live-geo-lng, --live-geo-accuracy (metres) |
cpuPressure | cpu-pressure | --live-cpu-pressure (CPU Compute Pressure as an ordered tier: nominal=0, fair=1, serious=2, critical=3) |
Element plugins
Section titled “Element plugins”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 -->| Import | Key | Properties |
|---|---|---|
pointerLocal | pointer-local | --live-local-pointer-x-ratio, --live-local-pointer-y-ratio (0–1 within the element’s box), --live-local-pointer-inside (1 / 0) |
media | media | --live-current-time, --live-duration, --live-progress (0–1), --live-paused (1 / 0), --live-volume (0–1), for <video> / <audio> |
field | field | --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 (0–1) for a pure-CSS counter (neither written without a maxlength — keep a var() fallback) — see the Field demo |
select | select | a <select>’s state as numbers CSS can’t reach: --live-index (selectedIndex, -1 if none), --live-option-count, --live-index-pct (0–1), --live-value-num (Number(value), only when the value is numeric), --live-selected-count and --live-selected-pct (0–1) — 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 |
colorInput | color-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 |
fieldState | field-state | the 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 |
formState | form-state | form-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 (0–1, valid required ÷ required). Bind a <form> / wrapper — see the Form state demo |
img | img | --live-natural-w, --live-natural-h (intrinsic pixel size), --live-loaded (1 / 0), --live-broken (1 / 0), for <img> |
imgColor | img-color | a 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 |
videoColor | video-color | live 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 |
Permissions & feature detection
Section titled “Permissions & feature detection”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.
Planned
Section titled “Planned”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).
Intentionally excluded
Section titled “Intentionally excluded”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 thescroll-velocityplugin.