01

Tokens & curves

Three damping ratios, one generator, zero hand-edited stops. Each curve is the closed-form step response of an underdamped spring, sampled into CSS linear() — it overshoots, dips back UNDER 1.0, then settles. cubic-bezier physically cannot express that second crossing.

--ease-spring-pop ζ.62 +8%--ease-spring ζ.48 +17%--ease-spring-bounce ζ.34 +32%

Step responses

progress vs. time — watch each dip under the 1.0 line

1.0
pop ζ0.62spring ζ0.48bounce ζ0.34

Feel the difference

each dot targets the same line — only the physics differ

pop
380ms
spring
500ms
bounce
620ms

The closed vocabulary

every duration has a role — there is no eighth tier

tokenvalueeasingrole
--motion-base160ms--easecolor / border / bg — the default transition
--motion-pop380msspring ζ.62hover + press release
--motion-spring500msspring ζ.48enters · FLIP · list rise
--motion-bounce620msspring ζ.34one-shot celebration
--motion-exit100ms--ease-in-softEVERY close — flat, never a spring

Two rules live IN the values: exits are ~2/3 of the shortest enter and always flat — a spring on the way out reads as hesitation. And the spring tiers are deliberately LONGER than the plain tiers: the settle tail after the overshoot IS the feel.

Generator recipe

do not hand-edit stops — rerun the script

$ node scripts/spring-easing.mjs

x(t) = 1 - e^(-ζωt)·( cos(ω_d t) + (ζ/√(1-ζ²))·sin(ω_d t) )   ω_d = ω√(1-ζ²)

1. pick ζ        peak overshoot = exp(-πζ/√(1-ζ²))
                 ζ.62→+8% pop · ζ.48→+17% spring · ζ.34→+32% bounce
2. normalize     ωT = ln(1/0.001)/ζ  → envelope decays to 0.1% at t=1
3. sample        24 uniform stops, 4 decimals, clamp last to 1
4. paste into    globals.css under @supports (animation-timing-function:
                 linear(0,1)) — cubic-bezier fallbacks stay above