Animation system
Every component in the library animates through one spring integrator and one shared frame loop. There is no animation library dependency.
The integrator
Springs are integrated with semi-implicit Euler at a fixed 1/240s substep. Fixed substepping keeps the simulation deterministic regardless of frame rate — these springs run at high stiffness, where a single long frame would otherwise make a variable-step integration overshoot badly or diverge entirely.
A spring reports itself settled once both its displacement and its velocity fall below a precision threshold, at which point it stops consuming frames. That matters on a page like this one, where dozens of components are mounted at once but only the one under the cursor should be doing work.
One loop for everything
Rather than each spring starting its own requestAnimationFrame loop, all of them subscribe to a single ticker. A page with fifty animated components still schedules one callback per frame, and the loop cancels itself entirely when nothing is animating.
Presets
Every motion prop accepts a preset name or an explicit { stiffness, damping, mass } object.
| Preset | Stiffness | Damping | Mass | Use for |
|---|---|---|---|---|
| bouncy | 380 | 18 | 1 | Visible overshoot. The default, and the house style. |
| smooth | 220 | 18 | 1 | Wobbly with the ring taken out. The restrained option. |
| wobbly | 180 | 12 | 1 | Long, loose wobble. Cords, pendulums, trailing elements. |
| elastic | 300 | 10 | 1 | Keeps ringing. Elastic deformation. |
Compare presets
Hooks
useSpringValue is the workhorse. It never triggers a React re-render — you subscribe with onChange and write straight to the DOM, which is what keeps high-frequency effects like magnetic tracking and drag cheap.
useSpringState is the re-rendering variant, for values that feed JSX which cannot be expressed as a style write — interpolated SVG path data, for example.
import { useSpringValue } from "tzuu-ui";
function Follower() {
const ref = useRef<HTMLDivElement>(null);
const x = useSpringValue(0, "bouncy");
// Subscribe once; writes bypass React entirely.
useEffect(
() =>
x.onChange((value) => {
ref.current?.style.setProperty("--x", `${value}px`);
}),
[x],
);
return <div ref={ref} onClick={() => x.set(200)} />;
}Behaviour primitives
| Hook | Behaviour |
|---|---|
| usePress | Volume-preserving squash and stretch. Y compresses while X widens, so a press reads as compression rather than the element receding. |
| useMagnetic | Pulls an element toward the pointer with linear distance falloff, fed through a spring so it never feels glued to the cursor. Skipped on coarse pointers. |
| useDrag | Pointer drag with rubber-banded bounds and velocity sampled over a trailing window, so a flick is not scored off one noisy frame. Release velocity becomes a spring impulse. |
| useMorph | Springs an array of geometry numbers. Icon states are expressed as same-length arrays so shape interpolation is valid. |
| useReducedMotion | Every spring reads this and snaps to its target instead of simulating when the user prefers reduced motion. |
Reduced motion
Springs cannot be disabled by a CSS media query, so the check happens in JavaScript. When prefers-reduced-motion is set, every spring jumps straight to its target and stops subscribing to the ticker — state changes still happen, they just arrive instantly. Drag interactions remain functional, since they are direct manipulation rather than animation.
