animaly
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.

A spring with default options
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.

Stiffness: how hard the spring pulls
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.

Damping: how quickly the bounce dies out
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.

Mass: a heavier object moves slower and swings 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.
springs.ts: reusable presets
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.

A spring that settles in a fixed time
import { animate } from "@animaly/dom";

animate(".box", { x: 240 }, { type: "spring", duration: 0.6 });
Fixed-duration spring, then the next step
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.

Stop earlier with larger rest thresholds
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.

Start with a velocity
import { animate } from "@animaly/dom";

animate(".box", { y: 0 }, { type: "spring", velocity: -1200, stiffness: 300, damping: 12 });
Wait before the spring starts
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.

Retarget mid-flight: click while the ball moves
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();
Hover in and out without jumps
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));
Press feedback on a button
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.

Spring from a start value
import { animate } from "@animaly/dom";

animate(".modal", { scale: [0.85, 1], opacity: [0, 1] }, { type: "spring", stiffness: 280, damping: 22 });
Spring a size and a radius
import { animate } from "@animaly/dom";

animate(".panel", { width: 360, borderRadius: 24 }, { type: "spring", stiffness: 200, damping: 18 });
Spring a color
import { animate } from "@animaly/dom";

animate(".chip", { backgroundColor: "#7c3aed", color: "#ffffff" }, { type: "spring", stiffness: 200, damping: 30 });
Spring a motion value and read its velocity
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 });
Spring a plain number into a counter
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.

One spring, many elements
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.

Follow the pointer with a lag
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" });
A trail of dots, each one softer
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);
A switch thumb
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.

Stop a spring 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

Spring options
OptionTypeDefaultWhat it does
type"spring"tweenSelects the spring driver.
stiffnessnumber100How hard the spring pulls toward the target. Higher is faster.
dampingnumber10Friction against the motion. Lower values bounce more.
massnumber1Weight of the moving object. Heavier is slower and swings longer.
velocitynumbercurrentStarting velocity in units per second. Defaults to the value's current velocity.
durationnumbernoneSeconds the spring takes to settle. animaly derives the physics from it.
delaynumber | stagger()0Seconds to wait before starting.
restDeltanumbernoneDistance from the target below which the spring may stop.
restSpeednumbernoneSpeed below which the spring may stop, in units per second.