animaly
Documentation menu

Guides

Motion values, numbers and objects

Not everything you animate is a CSS property. Motion values, plain numbers, color strings and objects run on the same engine and hand you the latest value once per frame.

Create a motion value

motionValue holds a number or a string. Read it with get, change it with set, and subscribe with on. Every on call returns a function that removes the listener.

Create and read a motion value
import { motionValue } from "@animaly/dom";

const progress = motionValue(0);
const label = motionValue("0px");

console.log(progress.get()); // 0
console.log(label.get()); // "0px"
Set a value and listen for changes
import { motionValue } from "@animaly/dom";

const volume = motionValue(0.5);

const unsubscribe = volume.on("change", (latest) => {
  console.log("volume is now", latest);
});

volume.set(0.8); // logs "volume is now 0.8" right away
unsubscribe();
volume.set(0.2); // no log, the listener is gone

set and jump

set changes the value, notifies change listeners right away and stops any animation running on the value. jump does the same and also resets the velocity to zero, so the next spring starts from rest.

set keeps the previous value, jump resets velocity
import { motionValue } from "@animaly/dom";

const x = motionValue(0);

x.set(100);
console.log(x.get(), x.getPrevious()); // 100 0

x.jump(40);
console.log(x.get(), x.getVelocity()); // 40 0

Animate a motion value

Pass the value as the target of animate. Springs, tweens, keyframes and inertia all work. Animated changes reach listeners once per frame.

Animate a motion value with a spring
import { animate, motionValue } from "@animaly/dom";

const scale = motionValue(1);
scale.on("change", (latest) => console.log(latest.toFixed(3)));

animate(scale, 1.5, { type: "spring", stiffness: 300, damping: 15 });
Tween a motion value through keyframes
import { animate, motionValue } from "@animaly/dom";

const opacity = motionValue(0);

await animate(opacity, [0, 1, 0.4], { duration: 0.6, ease: "easeOut" });
console.log(opacity.get()); // 0.4

Velocity

getVelocity returns units per second. Start a new animation while one is running and the new spring takes over the current velocity.

Read velocity while a value is moving
import { animate, motionValue } from "@animaly/dom";

const x = motionValue(0);
animate(x, 400, { type: "spring" });

setTimeout(() => {
  console.log("px/s", Math.round(x.getVelocity()), "moving", x.isAnimating());
}, 100);
Retarget a value mid-flight
import { animate, motionValue } from "@animaly/dom";

const x = motionValue(0);
animate(x, 300, { type: "spring" });

setTimeout(() => {
  animate(x, -100, { type: "spring" });
}, 150);

Stopping

Stop a value where it is
import { animate, motionValue } from "@animaly/dom";

const x = motionValue(0);
animate(x, 500, { duration: 2 });

setTimeout(() => {
  x.stop();
  console.log("stopped at", Math.round(x.get()));
}, 300);

Render a value yourself

A motion value does not write to the DOM on its own. Subscribe to change and apply the value wherever you need it: a transform, a canvas, a WebGL uniform.

Write a motion value to the DOM yourself
import { animate, motionValue } from "@animaly/dom";

const ball = document.querySelector<HTMLElement>(".ball")!;
const x = motionValue(0);

x.on("change", (latest) => {
  ball.style.transform = `translateX(${latest}px)`;
});

animate(x, 240, { type: "spring", stiffness: 180, damping: 14 });
Follow the pointer with springs
import { animate, motionValue } from "@animaly/dom";

const dot = document.querySelector<HTMLElement>(".dot")!;
const x = motionValue(0);
const y = motionValue(0);

const render = (): void => {
  dot.style.transform = `translate(${x.get()}px, ${y.get()}px)`;
};
x.on("change", render);
y.on("change", render);

window.addEventListener("pointermove", (event) => {
  animate(x, event.clientX, { type: "spring", stiffness: 400, damping: 30 });
  animate(y, event.clientY, { type: "spring", stiffness: 400, damping: 30 });
});

Animate plain numbers

animate(from, to, options) animates a number without creating a motion value. onUpdate receives the latest value every frame.

Count up a number in text
import { animate } from "@animaly/dom";

const count = document.querySelector<HTMLElement>(".count")!;
const format = new Intl.NumberFormat("en-US");

animate(0, 12840, {
  duration: 1.4,
  ease: "easeOut",
  onUpdate: (latest) => {
    count.textContent = format.format(Math.round(latest));
  },
});
Animate between two prices
import { animate } from "@animaly/dom";

const price = document.querySelector<HTMLElement>(".price")!;

function showPrice(from: number, to: number): void {
  animate(from, to, {
    duration: 0.5,
    onUpdate: (latest) => {
      price.textContent = `$${latest.toFixed(2)}`;
    },
  });
}

showPrice(19, 190);

Animate strings and colors

Strings animate number by number: "0px" to "240px", or a whole box-shadow. Colors interpolate when both ends are hex, rgb() or rgba().

Animate a color for a canvas fill
import { animate } from "@animaly/dom";

const canvas = document.querySelector<HTMLCanvasElement>(".swatch")!;
const context = canvas.getContext("2d")!;

animate("#ffffff", "#7c3aed", {
  duration: 0.8,
  onUpdate: (color) => {
    context.fillStyle = color;
    context.fillRect(0, 0, canvas.width, canvas.height);
  },
});
Animate a string with units
import { animate, motionValue } from "@animaly/dom";

const bar = document.querySelector<HTMLElement>(".bar")!;
const width = motionValue("0px");

width.on("change", (latest) => {
  bar.style.width = latest;
});

animate(width, "240px", { duration: 0.6 });
Animate a complex string value
import { animate } from "@animaly/dom";

const card = document.querySelector<HTMLElement>(".card")!;

animate("0px 0px 0px rgba(0, 0, 0, 0)", "0px 12px 24px rgba(0, 0, 0, 0.25)", {
  duration: 0.3,
  onUpdate: (shadow) => {
    card.style.boxShadow = shadow;
  },
});

Named colors such as red are not parsed and throw an error. Use hex, rgb() or hsl().

Animate objects

Pass an object, or an array of objects, and the numeric or string properties to reach. animaly writes the properties on the object itself and calls onUpdate once per frame with the whole object, after every property is written.

Animate a camera object for a canvas
import { animate } from "@animaly/dom";

const canvas = document.querySelector<HTMLCanvasElement>(".scene")!;
const context = canvas.getContext("2d")!;
const camera = { zoom: 1, x: 0, y: 0 };

function draw(view: typeof camera): void {
  context.setTransform(view.zoom, 0, 0, view.zoom, -view.x, -view.y);
  context.clearRect(0, 0, 1000, 1000);
  context.fillStyle = "#7c3aed";
  context.fillRect(140, 80, 40, 40);
}

animate(camera, { zoom: 2, x: 160, y: 100 }, { type: "spring", stiffness: 120, damping: 20, onUpdate: draw });
Animate many objects at once
import { animate, stagger } from "@animaly/dom";

const particles = Array.from({ length: 50 }, () => ({ x: 0, y: 0, alpha: 1 }));

animate(particles, { x: 300, alpha: 0 }, { duration: 1.2, delay: stagger(0.02) });

Events

Motion value events
OptionTypeDefaultWhat it does
change(latest) => voidnoneThe value changed: right away for set and jump, once per frame while animating.
animationStart() => voidnoneAn animation started on the value.
animationComplete() => voidnoneThe animation reached its end.
animationCancel() => voidnoneThe running animation ended early: stop, set, or a new animation replaced it.
destroy() => voidnoneThe value was destroyed.
React to an animation starting and finishing
import { animate, motionValue } from "@animaly/dom";

const progress = motionValue(0);

progress.on("animationStart", () => console.log("started"));
progress.on("animationComplete", () => console.log("finished at", progress.get()));
progress.on("animationCancel", () => console.log("cancelled"));

animate(progress, 100, { duration: 0.4 });

Destroying values

Call destroy when a value is no longer used. It stops animations, frees its place in the engine and removes every listener.

Destroy a value you no longer need
import { motionValue } from "@animaly/dom";

const x = motionValue(0);
x.on("destroy", () => console.log("x was destroyed"));

x.destroy();

Method summary

  • get() the current value; getPrevious() the value before the last change.
  • set(value) and jump(value) change the value and stop animations; jump also clears velocity.
  • getVelocity() units per second; isAnimating() whether an animation is running.
  • stop() holds the value where it is; destroy() frees it.
  • on(event, listener) subscribes and returns an unsubscribe function.