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
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
| token | value | easing | role |
|---|---|---|---|
| --motion-base | 160ms | --ease | color / border / bg — the default transition |
| --motion-pop | 380ms | spring ζ.62 | hover + press release |
| --motion-spring | 500ms | spring ζ.48 | enters · FLIP · list rise |
| --motion-bounce | 620ms | spring ζ.34 | one-shot celebration |
| --motion-exit | 100ms | --ease-in-soft | EVERY 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