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.
import { motionValue } from "@animaly/dom";
const progress = motionValue(0);
const label = motionValue("0px");
console.log(progress.get()); // 0
console.log(label.get()); // "0px"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 goneset 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.
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 0Animate 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.
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 });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.4Velocity
getVelocity returns units per second. Start a new animation while one is running and the new spring takes over the current velocity.
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);import { animate, motionValue } from "@animaly/dom";
const x = motionValue(0);
animate(x, 300, { type: "spring" });
setTimeout(() => {
animate(x, -100, { type: "spring" });
}, 150);Stopping
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.
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 });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.
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));
},
});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().
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);
},
});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 });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.
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 });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
| Option | Type | Default | What it does |
|---|---|---|---|
| change | (latest) => void | none | The value changed: right away for set and jump, once per frame while animating. |
| animationStart | () => void | none | An animation started on the value. |
| animationComplete | () => void | none | The animation reached its end. |
| animationCancel | () => void | none | The running animation ended early: stop, set, or a new animation replaced it. |
| destroy | () => void | none | The value was destroyed. |
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.
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)andjump(value)change the value and stop animations;jumpalso 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.