clock
00 / auto
Quick start: just an attribute
import 'prop-for-that/auto' once, then tag any element
with data-props-for and list the props you want.
×
import 'prop-for-that/auto'
<div data-props-for="size" style="resize: both"></div>
.box::after {
counter-reset: w calc(var(--live-w)) h calc(var(--live-h));
content: counter(w) ' × ' counter(h);
}
01 / pointer
Track the pointer
Bind pointer-local to an element and it gets its own
--live-local-pointer-x-ratio / --live-local-pointer-y-ratio props.
local
move your
pointertilt your
device
the panel follows, then rests
Motion blocked · needs a mouse here
<div id="card" data-props-for="pointer-local">…</div>
.tilt-card {
--rx: calc((.5 - var(--live-local-pointer-y-ratio)) * 16deg);
--ry: calc((var(--live-local-pointer-x-ratio) - .5) * 16deg);
transform: rotateX(var(--rx)) rotateY(var(--ry));
}
02 / scroll
Reveal once, then stay
Triggered, not linked. --const-has-entered latches once a
panel is fully in view, so each reveals once and stays. Scroll back up: a
view() timeline would reverse, this won’t.
Each step develops in as it enters — lift, unblur, fade — then holds.
--live-visible lights this card only while it’s wholly on screen.
One element, two signals — live toggles, entered latches.
in view entered
Scroll back up: in view dims, entered stays. No reset.
03 / range
One value, many readers
Bind one source to the container; the gauge ring, the number, and
the slider all read the same value. At either end, a
@container style() query flips a min / max
state that var() can’t express.
<div id="meter" data-props-for="range">
<input type="range" min="0" max="100" value="42">
</div>
.gauge__num::after {
counter-reset: v calc(var(--live-value));
content: counter(v);
}
.gauge__arc {
background: conic-gradient(var(--accent)
calc(var(--live-value-pct) * 360deg), var(--line) 0);
}
var()@container style(--live-value: 100) {
.gauge__num { color: var(--max-tint); }
.gauge__flag::after { content: 'max'; }
}
04 / style query
Discrete state, whole rules
var() interpolates a number; it can’t turn
3 into the word “high” or switch a rule on at
one exact value. @container style() can: each level
lights a different block of CSS, zero JS branches.
<div id="level" data-props-for="range">
<input type="range" min="0" max="4" step="1" value="2">
</div>
@container style(--live-value: 0) {
.level__word::after { content: 'Off'; }
}
@container style(--live-value: 4) {
.level__word::after { content: 'Max'; }
.level__core { --tier-h: 25; }
}
05 / field
Length, live as you type
The limits stay in HTML (minlength,
maxlength); CSS reads the live length against them to
fill the meter, count up, and name the state. No keystroke handler.
<div id="note" class="field" data-props-for="field" style="--min: 12; --max: 120">
<textarea minlength="12" maxlength="120"></textarea>
</div>
.field__meter i {
inline-size: calc(var(--live-length) / var(--max) * 100%);
}
@container style(--live-length: 120) {
.field__word::after { content: 'max reached'; }
}
06 / trig
Yes, CSS can do trigonometry
Each pupil finds the cursor with atan2(), then rides
there on cos() and sin(). The trigonometry
is all in the stylesheet; JS never computes an angle.
<div class="eye" data-props-for="pointer-local">…</div>
.eye {
--a: atan2(
calc(var(--live-local-pointer-y-ratio) - .5),
calc(var(--live-local-pointer-x-ratio) - .5));
}
.eye__pupil {
translate:
calc(cos(var(--a)) * 1.1rem)
calc(sin(var(--a)) * 1.1rem);
}
07 / clock
A clock, ticking in pure CSS
The clock source ticks --live-seconds once a second; the
hands rotate by calc() and the readout is a CSS counter.
The only JavaScript is one setInterval.
<div class="clock" data-props-for="clock">…</div>
.clock__hand--sec {
rotate: calc(var(--live-seconds) * 6deg);
}
.clock__hand--hour {
rotate: calc(var(--live-hours) * 30deg
+ var(--live-minutes) * .5deg);
}
08 / globals
Ambient device state
The quiet globals: online, link speed, battery, frame rate. Each
writes on :root for CSS to read. Toggle offline in
DevTools to watch it flip. (Battery and Network are Chromium-only.)
connection
navigator.onLine
effective type
Mbps · ms rtt
Chromium only
battery ⚡
Chromium only
frame rate
<div class="tiles" data-props-for="online network battery fps">…</div>
@container style(--live-online: 0) {
.tile--online { color: var(--limit); }
.tile__online::after { content: 'offline'; }
}
.tile__gauge i {
inline-size: calc(var(--live-battery-level) * 100%);
}
09 / velocity
Scrolling with momentum
--live-scroll-velocity is signed, and eases back to zero
when you stop. Flick the page: the bars shear and slide with your
speed, then settle.
<div class="momentum" data-props-for="scroll-velocity">…</div>
.momentum__stack span {
transform:
translateX(calc(var(--live-scroll-velocity) * var(--m) * 1px))
skewX(calc(var(--live-scroll-velocity) * -.4deg));
}
10 / size
A card that knows its own shape
Drag the corner: a ResizeObserver feeds --live-aspect,
and a branch-free calc() flips the badge from portrait to
landscape at aspect 1. No media query.
×
<div id="sizer" data-props-for="size" style="resize: both; overflow: auto"></div>
.sizer {
--landscape: clamp(0, calc((var(--live-aspect) - 1) * 100), 1);
}
.sizer__land { opacity: var(--landscape); }
.sizer__por { opacity: calc(1 - var(--landscape)); }
11 / media
A progress ring, straight from playback
A conic-gradient ring fills from
--live-progress as the clip plays. Typed as an
@property, the value interpolates, so the ring sweeps
smoothly between updates.
/
<div id="player" data-props-for="media">
<video src="clip.mp4" muted loop autoplay></video>
</div>
configure({ typed: true }) // register --live-* as @property
.player { transition: --live-progress .25s linear; }
.player__ring {
background: conic-gradient(var(--accent)
calc(var(--live-progress) * 360deg), var(--line) 0);
}
12 / ambient
UI that saves your battery
Below 20% battery, a @container style() strips the
animation and heavy paint to save power. Flip the switch to simulate
it. (Battery Status is Chromium-only.)
now playing
.ambient {
--low: clamp(0, round(up, calc(.2 - var(--live-battery-level, 1)), 1), 1);
}
@container style(--low: 1) {
.ambient__card { animation: none; }
.ambient__state::after { content: 'low battery'; }
}
13 / drag
Drag, with no drag handler
:active is the interaction gate; while you hold a token
its translate is computed straight from
--live-local-pointer-x-ratio / --live-local-pointer-y-ratio and the
board’s size. Let go and it springs home — JS never does
a drag calculation.
<div id="dragboard" data-props-for="pointer-local size">
<span class="drag-token">…</span>
</div>
.drag-token:active {
translate:
calc((var(--live-local-pointer-x-ratio) - var(--hx)) * var(--live-w) * 1px)
calc((var(--live-local-pointer-y-ratio) - var(--hy)) * var(--live-h) * 1px);
transition: none; /* follow while held */
}
14 / form state
The form state CSS can’t see
field-state tracks the interaction history pseudo-classes
can’t — --live-dirty, --live-touched,
--live-changed, --live-submitted, per input.
form-state aggregates the whole form —
--live-valid-count, --live-all-valid,
--live-completion — to drive the progress bar and gate
Submit. Type then delete back: dirty stays, changed
clears. Empty a required field: completion drops, the gate closes. The
required plan <select> (select →
--live-value-num) joins that gate, and the colour picker
(color-input → --live-color) re-themes the
whole form — live, in CSS.
<form data-props-for="form-state">
<input name="name" required data-props-for="field-state">
<select name="plan" required data-props-for="select">…</select>
<input type="color" data-props-for="color-input">
</form>
.form { --accent: var(--live-color); } /* the picker re-skins it */
.seats::after { content: counter(--live-value-num) ' seats'; }
@container style(--live-all-valid: 1) {
.submit { opacity: 1; } /* the gate opens */
}
15 / img-color
A shadow the colour of the picture
CSS can’t read an image’s pixels. img-color does
— it pulls a small palette (dominant, accent, dark, light, average)
into --live-img-*. The dominant tints a drop shadow
that pointer-local casts away from your cursor, so the cursor
is the sun; the swatches below show the rest. Pick another image and the
whole palette re-reads.
<figure data-props-for="img-color">
<img src="art.jpg">
</figure>
.sun-card { /* shadow: image colour, cast off the cursor */
box-shadow:
calc((.5 - var(--live-local-pointer-x-ratio)) * 8rem)
calc((.5 - var(--live-local-pointer-y-ratio)) * 8rem) 3rem
oklch(from var(--live-img) l c h / .6);
}
16 / video-color
A glow that watches the footage
The same idea, in motion. video-color samples a playing
<video> (~4×/sec, riding
requestVideoFrameCallback) for its dominant and
accent colours. The dominant pours into a box-shadow glow, so the
player looks backlit by whatever is on screen, easing colour to colour as
the shot changes; both swatches sit below.
<figure data-props-for="video-color">
<video src="clip.mp4" crossorigin muted loop autoplay></video>
</figure>
.player { /* backlit by the dominant, eased between samples */
box-shadow: 0 0 6rem
oklch(from var(--live-video) l c h / .7);
transition: box-shadow .3s linear;
}
ref / sources
Every source, at a glance
One key, and a source’s values land in --live-*.
Global sources write on :root, so they read live
here; element sources write on a bound node, so they show
— until you reach their demo.
pointerglobal- --live-pointer-x
- --live-pointer-y
- --live-pointer-x-ratio
- --live-pointer-y-ratio
Cursor position and 0–1 viewport ratios.
viewportglobal- --live-vw
- --live-vh
Window inner size, on resize.
clockglobal- --live-hours
- --live-minutes
- --live-seconds
- --live-now
Wall-clock time, every second.
fpsglobal- --live-fps
Live rendering frame rate.
onlineglobal- --live-online
Network reachability (0 / 1).
pageglobal- --live-page-focused
- --live-page-visible
Page state: window focused & tab visible (0 / 1).
networkglobalChromium- --live-net-type
- --live-net-downlink
- --live-net-rtt
- --live-net-save-data
Effective connection quality.
batteryglobalChromium- --live-battery-level
- --live-battery-charging
Power level and charging state.
cpu-pressureglobalChromium- --live-cpu-pressure
CPU compute pressure, 0–3.
scroll-velocityglobal- --live-scroll-velocity
- --live-scroll-direction
Signed scroll speed, decays to zero.
visual-viewportglobal- --live-vvp-scale
- --live-vvp-offset-top
- --live-vvp-height
Pinch-zoom and on-screen keyboard.
orientationglobalpermission- --live-orient-alpha
- --live-orient-beta
- --live-orient-gamma
Device tilt, in degrees.
motionglobalpermission- --live-accel-x
- --live-accel-y
- --live-accel-z
Accelerometer, m/s².
geoglobalpermission- --live-geo-lat
- --live-geo-lng
- --live-geo-accuracy
Geolocation, watched live.
headstartup- --const-dpr
- --const-cores
- --const-mem
- --const-scrollbar-w
- --const-scrollbar-thin-w
Write-once, FOUC-safe constants set in <head> before first paint.
nav-typeglobal- --const-nav-type
How you reached this page, set once: navigate / reload / back / prerender.
pointer-localelement- --live-local-pointer-x-ratio
- --live-local-pointer-y-ratio
- --live-local-pointer-inside
Cursor position within the element.
sizeelement- --live-w
- --live-h
- --live-aspect
Box size, via ResizeObserver.
visibilityelement- --live-visible
- --const-has-entered
Fully in view, plus a latch once entered.
rangeelement- --live-value
- --live-value-pct
A slider or number input’s value.
fieldelement- --live-length
- --live-empty
- --live-valid
- --live-remaining
- --live-fill-pct
Text-field length, validity, and remaining-character budget.
fieldelementvalidity- --live-value-missing
- --live-type-mismatch
- --live-pattern-mismatch
- --live-bad-input
- --live-custom-error
field flags which constraint failed: missing, wrong type, pattern, bad input, custom.
fieldelementvalidity- --live-too-short
- --live-too-long
- --live-range-underflow
- --live-range-overflow
- --live-step-mismatch
Length, range & step bounds: too long/short, under/over range, step mismatch.
field-stateelement- --live-dirty
- --live-pristine
- --live-touched
- --live-untouched
- --live-changed
- --live-submitted
Form interaction history pseudo-classes can’t see: edited/pristine, blurred/untouched, changed, submitted.
form-stateelement- --live-field-count
- --live-valid-count
- --live-invalid-count
- --live-all-valid
- --live-completion
Form-wide validity & completion CSS can’t count: valid/invalid totals, the all-valid gate.
selectelement- --live-index
- --live-option-count
- --live-index-pct
- --live-value-num
- --live-selected-count
- --live-selected-pct
A <select>’s choice as numbers: index, option count, and its numeric value.
color-inputelement- --live-color
The colour chosen in an <input type="color">, as a hex CSS can read.
mediaelement- --live-progress
- --live-current-time
- --live-duration
- --live-paused
- --live-volume
A <video> or <audio> element.
imgelement- --live-natural-w
- --live-natural-h
- --live-loaded
- --live-broken
Intrinsic size and load state of an <img>.
img-colorelement- --live-img
- --live-img-accent
- --live-img-dark
- --live-img-light
- --live-img-avg
- --live-img-temp
A 5-swatch palette (dominant, accent, dark, light, avg) + warm/cool temp, from an <img>’s pixels.
video-colorelement- --live-video
- --live-video-accent
Dominant colour of a playing <video>, sampled ~4×/sec for an ambient glow.