Interface · 05

Motion & Reaction

The animation vocabulary that goes with the layout and charting vocabulary. Two references sit behind it: the cursor-repel heading from finder, and the live-custom-property discipline from prop-for-that.

The second is the important one, and it is a rule rather than an effect: JavaScript writes numbers, CSS decides what they look like. A pointer handler's entire job is to set --live-pointer-x-ratio to a value between 0 and 1. Whether that becomes a tilt, a glow, a parallax or nothing at all is a stylesheet decision — so motion can be rethemed or switched off without touching script. Adopted as a pattern only: this library stays on vanilla CSS, hex colour and data-theme, not oklch, light-dark() or a webfont.

seed 3 Drives the reactive meter and stream below
01 A heading whose letters dodge your cursor put your pointer on the word below — the letters get out of the way

What this is. A page title where every letter is a separate element on a spring. Bring the pointer near one and it is pushed away; take the pointer off and it drifts back to where it belongs. That is the whole effect — there is nothing to click, no state, and no outcome. It exists to make a masthead feel like a physical surface rather than a printed one, and it is here because the same three numbers behind it (how far the pointer reaches, how hard it pushes, how the thing returns) are the numbers behind every other bit of motion on this page.

How it is built. Each glyph is a span carrying its own spring. The pointer pushes characters away with a linear falloff inside an 80px radius — the dashed circle that follows your cursor below is that radius, drawn; anything inside it is being pushed, anything outside it is not. When the pointer leaves, the glyphs settle back under damping rather than snapping. One requestAnimationFrame loop walks a flat array of glyphs — no per-character listeners and no CSS transitions, because a transition would fight the spring for control of transform. Under prefers-reduced-motion the loop never starts and the text stays ordinary, selectable type.

The reference setting
radius 80 · strength 18 · stiffness 0.08 · damping 0.78 — the two variants below change one group of these and nothing else

What the two cards below are for. They are the same effect with one group of numbers changed, side by side, so the parameters mean something you can feel instead of read. Sweep your pointer across all three at the same speed and compare: radius and strength decide how far the pointer's influence reaches and how hard it shoves — that is the outbound half; stiffness and damping decide how the glyphs travel home once it leaves — the return half. Tuning those two halves separately is the entire craft of spring motion.

Outbound changed — short reach, hard shove
radius 46 · strength 26. Compare with the reference: fewer letters react at once, because the circle of influence is smaller — but the ones that do react jump nearly half again as far.
Return changed — slow, loose, overshoots
stiffness 0.04 · damping 0.88. The push is identical to the reference; only the way home differs. Move the pointer off and watch — the letters drift back lazily and wobble past centre a couple of times before settling.
02 Live custom properties JS listens · CSS reacts

Two tiers of naming. --live-* properties change continuously and may be rewritten many times per second; --const-* properties latch once and are then left alone. The latch is what turns "animate in the first time this is seen" from a JavaScript state machine into a single CSS rule. All three demos below share exactly one helper each — the reactions are different only because the stylesheets are.

Pointer tilt
--live-pointer-x-ratio · --live-pointer-y-ratio
tilt

The handler writes two ratios. The rotation, the depth and the easing back to flat are all in the stylesheet — swapping tilt for parallax is a CSS edit.

Pointer spotlight
same two properties, different rules
identical JS

Byte-for-byte the same helper as the tile on the left — only the stylesheet differs, which is the whole argument for the pattern. Here the two ratios place a conic hue wheel and the radial mask that reveals it, so the spectrum rotates under the cursor instead of sliding past.

Reveal on entry
--live-visible · --const-has-entered
scrollobservelatchsettle
--live-visible 0.000 --const-has-entered 0

The four words start invisible and 28px low. An IntersectionObserver with 21 thresholds reports how much of the card is on screen as a number rather than a boolean, and that number is written to --live-visible; the words fade and rise in proportion to it, staggered 110ms apart. Once 15% of the card has been seen, --const-has-entered latches to 1 and pins them in place — so scrolling back and forth does not replay the entrance. That latch is why it looks like nothing happens: by the time the card is comfortably in view the reveal is already over. Replay re-arms the observer from zero.

03 Value-driven geometry one property · four readouts

Application state publishes a single --live-value-pct. A bar, a dial, a gradient stop and a text colour all read it. Nothing recalculates in script when the number moves — the frame budget goes to the compositor instead. Use the controls below to drive it: pause holds the last value so the four readouts can be compared against each other, and the FPS slider sets how fast the number is republished.

seed 3 Paused holds --live-value-pct; the readouts stay in sync because they all read the same property.
bar
dial
gradient
readout 32.4%
04 Easing & duration tokens hover a row to run it

Four easings and three durations cover everything in this library. The point of naming them is consistency across a codebase, not expressiveness: an interface where every transition was individually tuned reads as noise. --ease-out does most of the work — fast at the start, long settle, which is what makes an entrance feel decided rather than slow.

EasingCurveUsePreview
--ease-outcubic-bezier(.16, 1, .3, 1)entrances, reveals
--ease-in-outcubic-bezier(.65, 0, .35, 1)state swaps
--ease-springcubic-bezier(.34, 1.56, .64, 1)button presses
--ease-linearlinearprogress, streams
DurationValueUsePreview
--dur-fast120mshover, focus, press
--dur-base240mspanels, popovers
--dur-slow480mspage-level reveals
Every effect on this page respects prefers-reduced-motion: the repel loop does not start, and the transitions collapse to zero duration.