Documentation menu
Guides
Springs
A spring moves a value toward its target the way a physical spring would. animaly solves springs in closed form, so a spring knows its position and velocity at any time and a new target picks up from where the old one was.
A first spring
Pass type: "spring". Without other options the spring uses a stiffness of 100, a damping of 10 and a mass of 1.
import { animate } from "@animaly/dom";
animate(".box", { x: 240 }, { type: "spring" });Stiffness, damping and mass
Three numbers describe the spring. Each one changes a different part of the motion.
Stiffness
Stiffness is how hard the spring pulls toward the target. Raise it for faster motion.
import { animate } from "@animaly/dom";
animate(".soft", { x: 240 }, { type: "spring", stiffness: 60 });
animate(".hard", { x: 240 }, { type: "spring", stiffness: 600 });Damping
Damping slows the motion down. Low damping overshoots and bounces; high damping approaches the target without passing it.
import { animate } from "@animaly/dom";
animate(".bouncy", { y: 160 }, { type: "spring", stiffness: 200, damping: 6 });
animate(".calm", { y: 160 }, { type: "spring", stiffness: 200, damping: 30 });Mass
Mass is the weight of the moving object. A heavier object takes longer to start and keeps swinging longer.
import { animate } from "@animaly/dom";
animate(".light", { x: 240 }, { type: "spring", stiffness: 200, damping: 15, mass: 0.5 });
animate(".heavy", { x: 240 }, { type: "spring", stiffness: 200, damping: 15, mass: 3 });Presets
Most interfaces need a handful of springs. Keep them in one module and reuse them; SpringAnimation types the object.
gentle: stiffness 120, damping 20. Sheets, drawers and large panels.snappy: stiffness 400, damping 30. Buttons, menus and small controls, with almost no overshoot.bouncy: stiffness 300, damping 10. Badges, notifications and playful entrances.stiff: stiffness 700, damping 40. Feedback that should feel immediate.
import { animate, type SpringAnimation } from "@animaly/dom";
export const springs = {
gentle: { type: "spring", stiffness: 120, damping: 20 },
snappy: { type: "spring", stiffness: 400, damping: 30 },
bouncy: { type: "spring", stiffness: 300, damping: 10 },
stiff: { type: "spring", stiffness: 700, damping: 40 },
} satisfies Record<string, SpringAnimation>;
animate(".sheet", { y: [400, 0] }, springs.gentle);
animate(".cta", { scale: [0.9, 1] }, springs.snappy);
animate(".badge", { scale: [0, 1] }, springs.bouncy);Springs with a fixed duration
Pass duration when the spring has to finish in a known time, for example to line up with another animation. animaly derives the spring from it, and damping still controls how much it bounces.
import { animate } from "@animaly/dom";
animate(".box", { x: 240 }, { type: "spring", duration: 0.6 });import { animate } from "@animaly/dom";
await animate(".drawer", { x: [-320, 0] }, { type: "spring", duration: 0.5, damping: 26 });
animate(".hint", { opacity: [0, 1] }, { duration: 0.2 });When a spring stops
A spring stops once it is within restDelta of the target and slower than restSpeed. Larger thresholds end long tails of tiny movement sooner.
import { animate } from "@animaly/dom";
animate(".box", { x: 240 }, {
type: "spring",
stiffness: 150,
damping: 8,
restDelta: 2,
restSpeed: 10,
});Velocity and delay
velocity sets the starting speed in units per second, for example from a gesture. delay waits before the spring starts.
import { animate } from "@animaly/dom";
animate(".box", { y: 0 }, { type: "spring", velocity: -1200, stiffness: 300, damping: 12 });import { animate } from "@animaly/dom";
animate(".box", { x: 240 }, { type: "spring", delay: 0.4 });Retargeting mid-flight
Calling animate again on a value that is still moving starts the new spring from the current position with the current velocity. The motion turns around instead of jumping or stopping first.
import { animate } from "@animaly/dom";
const ball = document.querySelector<HTMLElement>(".ball")!;
const toggle = document.querySelector<HTMLButtonElement>(".toggle")!;
let right = false;
const send = (): void => {
right = !right;
animate(ball, { x: right ? 300 : 0 }, { type: "spring", stiffness: 120, damping: 12 });
};
toggle.addEventListener("click", send);
send();import { animate } from "@animaly/dom";
const card = document.querySelector<HTMLElement>(".card")!;
const spring = { type: "spring", stiffness: 300, damping: 20 } as const;
card.addEventListener("pointerenter", () => animate(card, { y: -6, scale: 1.03 }, spring));
card.addEventListener("pointerleave", () => animate(card, { y: 0, scale: 1 }, spring));import { animate } from "@animaly/dom";
const button = document.querySelector<HTMLButtonElement>(".button")!;
button.addEventListener("pointerdown", () => {
animate(button, { scale: 0.94 }, { type: "spring", stiffness: 600, damping: 30 });
});
button.addEventListener("pointerup", () => {
animate(button, { scale: 1 }, { type: "spring", stiffness: 400, damping: 12 });
});
button.addEventListener("pointerleave", () => {
animate(button, { scale: 1 }, { type: "spring", stiffness: 400, damping: 20 });
});Springs on other values
Springs work on any value animaly can animate: keyframes with a start value, sizes, colors, motion values and plain numbers.
import { animate } from "@animaly/dom";
animate(".modal", { scale: [0.85, 1], opacity: [0, 1] }, { type: "spring", stiffness: 280, damping: 22 });import { animate } from "@animaly/dom";
animate(".panel", { width: 360, borderRadius: 24 }, { type: "spring", stiffness: 200, damping: 18 });import { animate } from "@animaly/dom";
animate(".chip", { backgroundColor: "#7c3aed", color: "#ffffff" }, { type: "spring", stiffness: 200, damping: 30 });import { animate, motionValue } from "@animaly/dom";
const readout = document.querySelector<HTMLOutputElement>(".readout")!;
const progress = motionValue(0);
progress.on("change", (latest) => {
readout.textContent = `${latest.toFixed(1)} at ${Math.round(progress.getVelocity())}/s`;
});
animate(progress, 100, { type: "spring", stiffness: 120, damping: 14 });import { animate } from "@animaly/dom";
const count = document.querySelector<HTMLElement>(".count")!;
animate(0, 1280, {
type: "spring",
stiffness: 80,
damping: 20,
onUpdate: (latest) => {
count.textContent = Math.round(latest).toLocaleString();
},
});Many elements
One call can spring many elements; combine it with stagger for entrances.
import { animate, stagger } from "@animaly/dom";
animate(".dot", { y: [-40, 0], opacity: [0, 1] }, {
type: "spring",
stiffness: 260,
damping: 14,
delay: stagger(0.05),
});Following the pointer
Retarget a spring on every pointer move and the element follows with a natural lag. Softer springs lag more.
import { animate } from "@animaly/dom";
const cursor = document.querySelector<HTMLElement>(".cursor")!;
window.addEventListener("pointermove", (event) => {
animate(cursor, { x: event.clientX - 12, y: event.clientY - 12 }, { type: "spring", stiffness: 250, damping: 24, mass: 0.6 });
});
animate(cursor, { x: 200, y: 120 }, { type: "spring" });import { animate } from "@animaly/dom";
const dots = Array.from(document.querySelectorAll<HTMLElement>(".trail"));
const follow = (x: number, y: number): void => {
dots.forEach((dot, index) => {
animate(dot, { x, y }, { type: "spring", stiffness: 300 - index * 60, damping: 20 });
});
};
window.addEventListener("pointermove", (event) => follow(event.clientX, event.clientY));
follow(160, 120);import { animate } from "@animaly/dom";
const toggle = document.querySelector<HTMLButtonElement>(".switch")!;
const thumb = toggle.querySelector<HTMLElement>(".thumb")!;
const set = (on: boolean): void => {
toggle.setAttribute("aria-checked", String(on));
animate(thumb, { x: on ? 22 : 0 }, { type: "spring", stiffness: 500, damping: 30 });
animate(toggle, { backgroundColor: on ? "#7c3aed" : "#d4d4d4" }, { duration: 0.2 });
};
toggle.addEventListener("click", () => set(toggle.getAttribute("aria-checked") !== "true"));
set(true);Stopping a spring
stop() on the returned controls leaves the element where it is.
import { animate } from "@animaly/dom";
const controls = animate(".box", { x: 400 }, { type: "spring", stiffness: 40, damping: 6 });
window.setTimeout(() => controls.stop(), 500);Springs run on the JS path by default. An animator made with createAnimator({ maxAccelerated: n }) runs springs that only touch x, y, rotate, scale and opacity as CSS animations with a linear() easing, for up to n elements.
Options
| Option | Type | Default | What it does |
|---|---|---|---|
| type | "spring" | tween | Selects the spring driver. |
| stiffness | number | 100 | How hard the spring pulls toward the target. Higher is faster. |
| damping | number | 10 | Friction against the motion. Lower values bounce more. |
| mass | number | 1 | Weight of the moving object. Heavier is slower and swings longer. |
| velocity | number | current | Starting velocity in units per second. Defaults to the value's current velocity. |
| duration | number | none | Seconds the spring takes to settle. animaly derives the physics from it. |
| delay | number | stagger() | 0 | Seconds to wait before starting. |
| restDelta | number | none | Distance from the target below which the spring may stop. |
| restSpeed | number | none | Speed below which the spring may stop, in units per second. |