Tiny, framework-agnostic physics spring animation — a closed-form damped harmonic oscillator with velocity-continuous interruption.
sprung (published on npm as sprungdesign) models motion as a real spring (mass · stiffness · damping) and solves it analytically, so it's frame-rate independent and exact at any timestep. Retarget mid-flight and the velocity carries over with no jump — the thing that makes spring UIs feel alive. Think in physics (stiffness/damping/mass) or in feel (duration/bounce).
- Zero dependencies, framework-agnostic core. SSR-safe — no DOM touched at import.
- Dual ESM + CJS, complete types, tree-shakeable (
sideEffects: false). - ~1 kB min+gzip for the core.
- Thin React adapter (
useSpring); more adapters can be layered on without touching the core.
npm install sprungdesignimport { useSpring } from "sprungdesign/react";
function Box({ open }: { open: boolean }) {
const x = useSpring(open ? 200 : 0, { stiffness: 320, damping: 14 });
return <div style={{ transform: `translateX(${x}px)` }} />;
}useSpring re-renders with the live value each frame and retargets velocity-continuously when target changes. It returns target on the server, honors prefers-reduced-motion by snapping instead of animating (evaluated at each retarget, not tracked live), and is StrictMode/concurrent-safe. config is read once on mount.
import { spring } from "sprungdesign";
const handle = spring({
stiffness: 180,
damping: 12,
onUpdate: (value) => {
el.style.transform = `translateX(${value}px)`;
},
});
el.addEventListener("click", () => handle.set(300));
// Call set() again mid-flight — the current velocity is preserved, no jump.import { fromFeel, spring } from "sprungdesign";
// bounce ∈ [-1, 1]: >0 bouncy · 0 critical (no overshoot) · <0 sluggish
// (the extremes are clamped to a settling range, so ±1 stay usable, not degenerate)
const handle = spring({ ...fromFeel({ duration: 0.5, bounce: 0.3 }), onUpdate });Named presets are included too:
import { presets, spring } from "sprungdesign";
spring({ ...presets.bouncy, onUpdate }); // gentle · bouncy · stiff · lazyThe pure solver. No side effects, no rAF — just math. at(t) samples the trajectory at t seconds.
const s = createSpring({ stiffness: 180, damping: 12, mass: 1, from: 0, to: 100, velocity: 0 });
s.at(0.25); // → { value, velocity, done }
s.zeta; // damping ratio (<1 underdamped, =1 critical, >1 overdamped)
s.w0; // natural angular frequency (rad/s)done is true once |value − to| < restDistance and |velocity| < restVelocity (both default 0.05); at that point value snaps exactly to to and velocity to 0.
The live, interruptible controller. Drives requestAnimationFrame; starts at rest at from and animates when you call set().
const handle = spring({ stiffness: 180, onUpdate: (value, velocity) => {}, onComplete: () => {} });
handle.set(target); // retarget — preserves current velocity (no jump)
handle.get(); // → { value, velocity }
handle.stop(); // freeze in placeAdvanced: pass now, raf, and caf to inject a custom clock/scheduler (for tests, a shared rAF loop, or fixed-timestep environments). Defaults are resolved lazily, so importing and constructing never touches the DOM.
Maps designer-friendly inputs to physics constants. duration sets the natural frequency; bounce sets the damping ratio (>0 underdamped, 0 critical, <0 overdamped).
gentle, bouncy, stiff, lazy — ready-made { stiffness, damping, mass } configs.
See Quick start. config is read once when the hook mounts.
SpringConfig, SpringState, Spring, SpringHandle, SpringControllerConfig, FeelOptions, SpringParams are all exported from sprung.
sprung solves m·x″ + c·x′ + k·x = 0 in closed form across all three damping regimes (under/critical/over). Because the solution is analytical, sampling is exact at any t and independent of frame rate. Interruption works by reading the current { value, velocity } and constructing a fresh solver anchored there — continuity is structural, not approximated. (The solver is validated against an independent RK4 integration across a wide parameter sweep.)
A full interactive playground (the spring.tuner) lives in examples/playground.