animaly
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

Pause and resume
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.

Create an animation without starting it
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.

Scrub with a range input
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;
});
Jump to a point in time
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 100

Scroll-linked animations

A paused animation whose time follows the scroll position. The scroll listener only writes a number; the engine renders it.

Drive an animation with scroll
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.

Replay a finished animation from a button
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());
Seek back into a finished animation
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.

Slow motion
import { animate } from "@animaly/dom";

const controls = animate(".box", { x: 240 }, { type: "spring", stiffness: 200, damping: 12 });
controls.speed = 0.25;
Double speed
import { animate } from "@animaly/dom";

const controls = animate(".box", { x: 240, rotate: 90 }, { duration: 1 });
controls.speed = 2;
Reverse mid-flight
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);
Flip direction on every click
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 resolves finished. onComplete runs.
  • stop() keeps the values where they are at that moment. finished does not resolve.
  • cancel() puts the values back to what they were before the call. finished does not resolve.
complete(): jump to the end
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
});
stop(): freeze where it is
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
});
cancel(): back to the start
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
});
Stop animations when a view goes away
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().

Await the end
import { animate } from "@animaly/dom";

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

await animate(dialog, { opacity: 0, y: 8 }, { duration: 0.2 });
dialog.remove();
Use the finished promise
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";
});
Wait for several animations
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");
onComplete callback
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.

Control a whole sequence
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));
Scrub a paused sequence
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.

duration and time include 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 call
Show progress while it runs
import { 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();
Pause while hovered
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

Members of the controls object
OptionTypeDefaultWhat it does
timenumbernoneCurrent position in seconds, from the call, delays included. Can be set.
speednumber1Playback rate. Negative values play backward, except for inertia.
durationnumbernoneTotal length in seconds, including delays and repeats. Read-only.
finishedPromise<void>noneResolves when the animation completes on its own or through complete().
then(onResolve, onReject) => PromisenoneMakes the controls awaitable; same as finished.then.
pause()() => voidnoneHolds the current position.
play()() => voidnoneResumes from the current position.
complete()() => voidnoneJumps to the end and resolves finished.
stop()() => voidnoneEnds the animation and keeps the current values.
cancel()() => voidnoneEnds the animation and restores the values from before the call.