Prop-for-that: CSS reacts, JavaScript just listens

tobr2 pts0 comments

prop-for-that: CSS reacts, JS just listens

pointer.x

pointer.y

00

00 / auto

Quick start: just an attribute

import 'prop-for-that/auto' once, then tag any element<br>with data-props-for and list the props you want.

data-props-for="size"

&thinsp;&times;&thinsp;

JS<br>import 'prop-for-that/auto'<br>HTML<br>div data-props-for="size" style="resize: both">div><br>CSS<br>.box::after {<br>counter-reset: w calc(var(--live-w)) h calc(var(--live-h));<br>content: counter(w) ' × ' counter(h);

01

01 / pointer

Track the pointer

Bind pointer-local to an element and it gets its own<br>--live-local-pointer-x-ratio&thinsp;/&thinsp;--live-local-pointer-y-ratio props.

local

move your<br>pointertilt your<br>device

the panel follows, then rests

Enable tilt

Motion blocked &middot; needs a mouse here

HTML<br>div id="card" data-props-for="pointer-local">…div><br>CSS<br>.tilt-card {<br>--rx: calc((.5 - var(--live-local-pointer-y-ratio)) * 16deg);<br>--ry: calc((var(--live-local-pointer-x-ratio) - .5) * 16deg);<br>transform: rotateX(var(--rx)) rotateY(var(--ry));

02

02 / scroll

Reveal once, then stay

Triggered, not linked. --const-has-entered latches once a<br>panel is fully in view, so each reveals once and stays. Scroll back up: a<br>view() timeline would reverse, this won&rsquo;t.

HTML<br>article class="reveal" data-props-for="visibility">…article><br>CSS<br>.reveal {<br>opacity: calc(.25 + var(--const-has-entered) * .75);<br>translate: 0 calc((1 - var(--const-has-entered)) * 1.5rem);<br>transition: .5s;

scroll the steps &darr;

Each step develops in as it enters — lift, unblur, fade — then holds.

ii

--live-visible lights this card only while it&rsquo;s wholly on screen.

iii

One element, two signals — live toggles, entered latches.

in view<br>entered

iv

Scroll back up: in view dims, entered stays. No reset.

03

03 / range

One value, many readers

Bind one source to the container; the gauge ring, the number, and<br>the slider all read the same value. At either end, a<br>@container style() query flips a min&thinsp;/&thinsp;max<br>state that var() can&rsquo;t express.

and<br>writes --live-value / --live-value-pct HERE, so both the gauge<br>(a separate element) and the slider inherit the value. -->

value

&larr;&rarr;

HTML<br>div id="meter" data-props-for="range"><br>input type="range" min="0" max="100" value="42"><br>div><br>CSS<br>.gauge__num::after {<br>counter-reset: v calc(var(--live-value));<br>content: counter(v);<br>.gauge__arc {<br>background: conic-gradient(var(--accent)<br>calc(var(--live-value-pct) * 360deg), var(--line) 0);<br>Discrete state: a style query, not var()<br>@container style(--live-value: 100) {<br>.gauge__num { color: var(--max-tint); }<br>.gauge__flag::after { content: 'max'; }

04

04 / style query

Discrete state, whole rules

var() interpolates a number; it can&rsquo;t turn<br>3 into the word &ldquo;high&rdquo; or switch a rule on at<br>one exact value. @container style() can: each level<br>lights a different block of CSS, zero JS branches.

and<br>writes --live-value (0–4) HERE, so the word and the stops below<br>inherit it and can @container style() the exact integer. -->

level

&uarr;&darr;

offlowmidhighmax

HTML<br>div id="level" data-props-for="range"><br>input type="range" min="0" max="4" step="1" value="2"><br>div><br>CSS<br>@container style(--live-value: 0) {<br>.level__word::after { content: 'Off'; }<br>@container style(--live-value: 4) {<br>.level__word::after { content: 'Max'; }<br>.level__core { --tier-h: 25; }

05

05 / field

Length, live as you type

The limits stay in HTML (minlength,<br>maxlength); CSS reads the live length against them to<br>fill the meter, count up, and name the state. No keystroke handler.

short bio<br>min 12 &middot; max 120

HTML<br>div id="note" class="field" data-props-for="field" style="--min: 12; --max: 120"><br>textarea minlength="12" maxlength="120">textarea><br>div><br>CSS<br>.field__meter i {<br>inline-size: calc(var(--live-length) / var(--max) * 100%);<br>@container style(--live-length: 120) {<br>.field__word::after { content: 'max reached'; }

06

06 / trig

Yes, CSS can do trigonometry

Each pupil finds the cursor with atan2(), then rides<br>there on cos() and sin(). The trigonometry<br>is all in the stylesheet; JS never computes an angle.

HTML<br>div class="eye" data-props-for="pointer-local">…div><br>CSS<br>.eye {<br>--a: atan2(<br>calc(var(--live-local-pointer-y-ratio) - .5),<br>calc(var(--live-local-pointer-x-ratio) - .5));<br>.eye__pupil {<br>translate:<br>calc(cos(var(--a)) * 1.1rem)<br>calc(sin(var(--a)) * 1.1rem);

07

07 / clock

A clock, ticking in pure CSS

The clock source ticks --live-seconds once a second; the<br>hands rotate by calc() and the readout is a CSS counter.<br>The only JavaScript is one setInterval.

HTML<br>div class="clock" data-props-for="clock">…div><br>CSS<br>.clock__hand--sec {<br>rotate: calc(var(--live-seconds) * 6deg);<br>.clock__hand--hour {<br>rotate: calc(var(--live-hours) * 30deg<br>+ var(--live-minutes) * .5deg);

08

08 / globals

Ambient device state

The quiet globals: online, link speed, battery, frame rate. Each<br>writes on :root for CSS to read. Toggle offline in<br>DevTools to watch it flip. (Battery and Network are...

live calc value pointer props style

Related Articles