Documentation menu
Guides
Playback controls
Every call to animate returns controls for all the values it started, including every element and every segment of a sequence. Use them to pause, scrub, change speed, end or await the animation.
Pause and play
import { animate } from "@animaly/dom";
const controls = animate(".box", { x: [0, 300] }, { duration: 3, ease: "linear" });
let paused = false;
document.querySelector(".toggle")!.addEventListener("click", () => {
paused ? controls.play() : controls.pause();
paused = !paused;
});Pause right after the call to set up an animation that starts later.
import { animate } from "@animaly/dom";
const controls = animate(".box", { rotate: 180, scale: 1.2 }, { duration: 0.6 });
controls.pause();
document.querySelector(".start")!.addEventListener("click", () => controls.play());Scrubbing
time is the position in seconds and can be set. Pause first, then set time from a slider, a scroll position or anything else. Multiply a 0 to 1 progress by duration to get the time.
import { animate } from "@animaly/dom";
const controls = animate(".box", { x: [0, 300], rotate: [0, 360] }, { duration: 2, ease: "linear" });
controls.pause();
const scrubber = document.querySelector<HTMLInputElement>(".scrubber")!;
scrubber.addEventListener("input", () => {
controls.time = Number(scrubber.value) * controls.duration;
});import { animate } from "@animaly/dom";
const controls = animate(".box", { x: [0, 400] }, { duration: 2, ease: "linear" });
controls.pause();
controls.time = 0.5; // a quarter of the way: x is 100Scroll-linked animations
A paused animation whose time follows the scroll position. The scroll listener only writes a number; the engine renders it.
import { animate } from "@animaly/dom";
const story = document.querySelector<HTMLElement>(".story")!;
const controls = animate(".progress-bar", { scaleX: [0, 1] }, { duration: 1, ease: "linear" });
controls.pause();
const update = (): void => {
const travel = story.offsetHeight - window.innerHeight;
const progress = travel > 0 ? Math.min(1, Math.max(0, window.scrollY / travel)) : 1;
controls.time = progress * controls.duration;
};
update();
window.addEventListener("scroll", update, { passive: true });Replaying a finished animation
A finished animation keeps its controls. play() runs it again from the start, and setting time moves it back to that point, so one call can serve a replay button or a scrubber after the first run.
import { animate } from "@animaly/dom";
const controls = animate(".badge", { scale: [0.6, 1.15, 1], opacity: [0, 1, 1] }, { duration: 0.5 });
// After it finishes, play() starts it again from the beginning.
document.querySelector(".replay")!.addEventListener("click", () => controls.play());import { animate } from "@animaly/dom";
const controls = animate(".bar", { scaleX: [0, 1] }, { duration: 1, ease: "linear" });
await controls.finished;
// Setting time on a finished animation moves it back to that point.
controls.pause();
controls.time = 0.25;Speed and direction
speed multiplies the playback rate. 0.25 is slow motion, 2 is twice as fast, and a negative value plays backward from the current position. Reverse playback works for tweens, keyframes and springs, but not for inertia.
import { animate } from "@animaly/dom";
const controls = animate(".box", { x: 240 }, { type: "spring", stiffness: 200, damping: 12 });
controls.speed = 0.25;import { animate } from "@animaly/dom";
const controls = animate(".box", { x: 240, rotate: 90 }, { duration: 1 });
controls.speed = 2;import { animate } from "@animaly/dom";
const controls = animate(".box", { x: [0, 300] }, { duration: 1.5, ease: "linear" });
setTimeout(() => {
controls.speed = -1; // plays back toward the start from where it is now
}, 600);import { animate } from "@animaly/dom";
const controls = animate(".box", { x: [0, 300] }, { duration: 4, ease: "linear" });
document.querySelector(".flip")!.addEventListener("click", () => {
controls.speed = -controls.speed;
});Ending an animation early
Three methods end an animation, and they differ in where the values stay and whether it counts as finished:
complete()jumps to the final values and resolvesfinished.onCompleteruns.stop()keeps the values where they are at that moment.finisheddoes not resolve.cancel()puts the values back to what they were before the call.finisheddoes not resolve.
import { animate } from "@animaly/dom";
const controls = animate(".box", { x: 300, opacity: 0.5 }, { duration: 3 });
document.querySelector(".skip")!.addEventListener("click", () => {
controls.complete(); // x is 300 now, and finished resolves
});import { animate } from "@animaly/dom";
const controls = animate(".box", { x: 300 }, { duration: 3 });
document.querySelector(".freeze")!.addEventListener("click", () => {
controls.stop(); // keeps the current x, finished does not resolve
});import { animate } from "@animaly/dom";
const controls = animate(".box", { x: 300 }, { duration: 3 });
document.querySelector(".undo")!.addEventListener("click", () => {
controls.cancel(); // x returns to its value before the call
});import { animate } from "@animaly/dom";
const controls = animate(".spinner", { rotate: [0, 360] }, { duration: 1, ease: "linear", repeat: 50 });
export function teardown(): void {
controls.stop();
}Waiting for the end
The controls are awaitable, and finished is the same moment as a promise. Both resolve when the animation completes on its own or through complete().
import { animate } from "@animaly/dom";
const dialog = document.querySelector<HTMLElement>(".dialog")!;
await animate(dialog, { opacity: 0, y: 8 }, { duration: 0.2 });
dialog.remove();import { animate } from "@animaly/dom";
const status = document.querySelector<HTMLElement>(".status")!;
const controls = animate(".box", { scale: 1.3 }, { duration: 0.4 });
controls.finished.then(() => {
status.textContent = "Finished";
});import { animate } from "@animaly/dom";
await Promise.all([
animate(".a", { x: 100 }, { duration: 0.4 }),
animate(".b", { x: 100 }, { type: "spring" }),
]);
console.log("both done");import { animate } from "@animaly/dom";
const status = document.querySelector<HTMLElement>(".status")!;
animate(".box", { rotate: 360 }, { duration: 0.8, onComplete: () => (status.textContent = "Spun") });Controlling a sequence
A sequence returns one set of controls. Pausing, scrubbing and speed apply to the whole timeline.
import { animate } from "@animaly/dom";
const controls = animate([
[".a", { x: 120 }, { duration: 0.6 }],
[".b", { x: 120 }, { duration: 0.6 }],
[".c", { x: 120 }, { duration: 0.6 }],
]);
document.querySelector(".pause")!.addEventListener("click", () => controls.pause());
document.querySelector(".resume")!.addEventListener("click", () => controls.play());
document.querySelector(".slow")!.addEventListener("click", () => (controls.speed = 0.3));import { animate } from "@animaly/dom";
const controls = animate([
[".a", { x: [0, 200] }, { duration: 1, ease: "linear" }],
[".b", { x: [0, 200], rotate: [0, 180] }, { duration: 1, ease: "linear" }],
]);
controls.pause();
const scrubber = document.querySelector<HTMLInputElement>(".scrubber")!;
scrubber.addEventListener("input", () => {
controls.time = Number(scrubber.value) * controls.duration;
});Reading time and duration
time counts from the moment of the call, so it includes delays. duration is the total length, including delays and repeats.
import { animate } from "@animaly/dom";
const controls = animate(".box", { x: 200 }, { duration: 0.6, delay: 0.5, repeat: 1 });
console.log(controls.duration); // 1.7: 0.5 s delay + two 0.6 s plays
console.log(controls.time); // 0 right after the callimport { animate } from "@animaly/dom";
const percent = document.querySelector<HTMLOutputElement>(".percent")!;
const controls = animate(".box", { x: [0, 300] }, { duration: 2 });
let running = true;
controls.finished.then(() => (running = false));
const tick = (): void => {
percent.value = `${Math.round((controls.time / controls.duration) * 100)}%`;
if (running) requestAnimationFrame(tick);
};
tick();import { animate } from "@animaly/dom";
const ticker = document.querySelector<HTMLElement>(".ticker")!;
const controls = animate(".track", { x: [0, -400] }, { duration: 8, ease: "linear", repeat: 20 });
ticker.addEventListener("pointerenter", () => controls.pause());
ticker.addEventListener("pointerleave", () => controls.play());Controls reference
| Option | Type | Default | What it does |
|---|---|---|---|
| time | number | none | Current position in seconds, from the call, delays included. Can be set. |
| speed | number | 1 | Playback rate. Negative values play backward, except for inertia. |
| duration | number | none | Total length in seconds, including delays and repeats. Read-only. |
| finished | Promise<void> | none | Resolves when the animation completes on its own or through complete(). |
| then | (onResolve, onReject) => Promise | none | Makes the controls awaitable; same as finished.then. |
| pause() | () => void | none | Holds the current position. |
| play() | () => void | none | Resumes from the current position. |
| complete() | () => void | none | Jumps to the end and resolves finished. |
| stop() | () => void | none | Ends the animation and keeps the current values. |
| cancel() | () => void | none | Ends the animation and restores the values from before the call. |