animaly
Documentation menu

Guides

Stagger and timelines

stagger spreads one animation across many elements with growing delays. A sequence is a list of animations placed on one timeline, controlled as a single animation.

Stagger

stagger(each) returns a function that delay accepts. Each element waits each seconds longer than the one before it.

Stagger a list in
import { animate, stagger } from "@animaly/dom";

animate(".item", { y: [12, 0], opacity: [0, 1] }, { duration: 0.35, delay: stagger(0.06) });

Where the stagger starts

from picks the element with no delay: "first" (the default), "last", "center" or an index. Delays grow with the distance from that element.

Ripple out from the middle
import { animate, stagger } from "@animaly/dom";

animate(".dot", { scale: [1, 1.6, 1] }, { duration: 0.5, delay: stagger(0.05, { from: "center" }) });
Start from the last element
import { animate, stagger } from "@animaly/dom";

animate(".item", { x: [-24, 0], opacity: [0, 1] }, { duration: 0.3, delay: stagger(0.05, { from: "last" }) });
Start from a specific index
import { animate, stagger } from "@animaly/dom";

const clicked = 2;

animate(".dot", { y: [0, -16, 0] }, { duration: 0.4, delay: stagger(0.04, { from: clicked }) });

A delay before the first element

Wait before the first element
import { animate, stagger } from "@animaly/dom";

animate(".item", { opacity: [0, 1] }, { duration: 0.3, delay: stagger(0.08, { startDelay: 0.4 }) });

Eased delays

ease distributes the delays along a curve instead of evenly. It accepts the same values as a tween's ease.

Ease the delays, not just the motion
import { animate, stagger } from "@animaly/dom";

// delays follow an easeOut curve across the row, so later elements bunch up
animate(".dot", { y: [20, 0], opacity: [0, 1] }, { duration: 0.4, delay: stagger(0.1, { ease: "easeOut" }) });

Staggered springs

Staggered springs
import { animate, stagger } from "@animaly/dom";

animate(".item", { x: [40, 0], opacity: [0, 1] }, { type: "spring", stiffness: 320, damping: 24, delay: stagger(0.05) });

Your own delay function

delay also accepts any function of (index, total) that returns seconds. This one measures the distance from the middle of a 5 by 5 grid, so the ripple is round instead of running along the list order.

Grid ripple from the center cell
import { animate } from "@animaly/dom";

const columns = 5;
const center = { x: 2, y: 2 };

const fromCenter = (index: number): number => {
  const x = index % columns;
  const y = Math.floor(index / columns);
  return Math.hypot(x - center.x, y - center.y) * 0.06;
};

animate(".cell", { scale: [0.6, 1], opacity: [0.2, 1] }, { duration: 0.45, delay: fromCenter });

Staggered exit

Stagger out, then remove
import { animate, stagger } from "@animaly/dom";

const items = Array.from(document.querySelectorAll<HTMLElement>(".item"));

await animate(items, { x: 24, opacity: 0 }, { duration: 0.2, delay: stagger(0.04, { from: "last" }) });
items.forEach((item) => item.remove());

Sequences

Pass an array of segments to animate. Each segment is [target, values] or [target, values, options]. By default a segment starts when the previous one ends.

Three steps, one after another
import { animate } from "@animaly/dom";

animate([
  [".a", { x: 120 }, { duration: 0.4 }],
  [".b", { x: 120 }, { duration: 0.4 }],
  [".c", { x: 120 }, { duration: 0.4 }],
]);

Placing segments with at

at moves a segment on the timeline. A number is an absolute time in seconds. "+0.2" starts 0.2 seconds after the previous segment ends, "-0.15" overlaps it by 0.15 seconds, and "<" starts together with the previous segment.

Start a segment at an absolute time
import { animate } from "@animaly/dom";

animate([
  [".a", { x: 120 }, { duration: 1 }],
  [".b", { opacity: 0.3 }, { duration: 0.3, at: 0.5 }],
]);
Gaps and overlaps
import { animate } from "@animaly/dom";

animate([
  [".a", { y: -20 }, { duration: 0.3 }],
  [".b", { y: -20 }, { duration: 0.3, at: "+0.2" }],
  [".c", { y: -20 }, { duration: 0.3, at: "-0.15" }],
]);
Run together with the previous segment
import { animate } from "@animaly/dom";

animate([
  [".a", { x: 100 }, { duration: 0.5 }],
  [".b", { rotate: 90 }, { duration: 0.5, at: "<" }],
]);

Labels

A string in the list marks the current end of the timeline with a name. { name, at } places a label at a time of your choice. Segments refer to a label through at.

Labels
import { animate } from "@animaly/dom";

animate([
  [".a", { x: 100 }, { duration: 0.4 }],
  "reveal",
  [".b", { opacity: [0, 1] }, { duration: 0.3, at: "reveal" }],
  [".c", { opacity: [0, 1] }, { duration: 0.3, at: "reveal" }],
  { name: "settle", at: 1.2 },
  [".a", { x: 0 }, { type: "spring", at: "settle" }],
]);

Options per property

Inside a segment, a key named after a property overrides the options for that property only. Here x springs while opacity tweens.

Different options per property
import { animate } from "@animaly/dom";

animate([
  [".a", { x: 160, opacity: 0.4 }, { duration: 0.6, x: { type: "spring", stiffness: 220, damping: 14 } }],
]);

Repeat and delay

Repeat a whole sequence after a delay
import { animate } from "@animaly/dom";

animate(
  [
    [".a", { scale: [1, 1.2, 1] }, { duration: 0.4 }],
    [".b", { scale: [1, 1.2, 1] }, { duration: 0.4 }],
    [".c", { scale: [1, 1.2, 1] }, { duration: 0.4 }],
  ],
  { repeat: 2, delay: 0.5 },
);

Default options for every segment

defaultTransition applies to segments that don't set their own options.

Defaults for every segment
import { animate } from "@animaly/dom";

animate(
  [
    [".a", { y: -24 }],
    [".b", { y: -24 }],
    [".c", { y: -24 }, { duration: 0.8 }],
  ],
  { defaultTransition: { duration: 0.25, ease: "easeOut" } },
);

Staggered segments

A segment that targets several elements can carry a stagger in its own delay.

A staggered segment inside a sequence
import { animate, stagger } from "@animaly/dom";

animate([
  [".list", { opacity: [0, 1] }, { duration: 0.2 }],
  [".item", { x: [-16, 0], opacity: [0, 1] }, { duration: 0.3, delay: stagger(0.05) }],
]);

Staggered delays inside a sequence work for tweens, keyframes and springs. Sequences can't contain inertia.

A hero entrance

Hero entrance: title, cards, button
import { animate, stagger } from "@animaly/dom";

animate([
  [".title", { y: [24, 0], opacity: [0, 1] }, { duration: 0.5, ease: "easeOut" }],
  [".lede", { y: [16, 0], opacity: [0, 1] }, { duration: 0.4, at: "-0.3" }],
  "cards",
  [".card", { y: [32, 0], opacity: [0, 1] }, { duration: 0.45, ease: "easeOut", at: "cards", delay: stagger(0.08) }],
  [".cta", { scale: [0.9, 1], opacity: [0, 1] }, { duration: 0.3, at: "+0.1" }],
]);

After a sequence

A sequence returns one set of controls for all its segments. Await it, or pass onComplete.

Wait for a sequence
import { animate } from "@animaly/dom";

const next = document.querySelector<HTMLButtonElement>(".next")!;

await animate([
  [".a", { opacity: [0, 1] }, { duration: 0.3 }],
  [".b", { opacity: [0, 1] }, { duration: 0.3 }],
  [".c", { opacity: [0, 1] }, { duration: 0.3 }],
]);
next.disabled = false;
Run code when a sequence ends
import { animate } from "@animaly/dom";

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

animate(
  [
    [".a", { x: 80 }, { duration: 0.3 }],
    [".b", { x: 80 }, { duration: 0.3 }],
  ],
  { onComplete: () => (status.textContent = "Done") },
);

duration covers the whole timeline, including repeats and the sequence delay.

Total length of a sequence
import { animate } from "@animaly/dom";

const controls = animate(
  [
    [".a", { x: 100 }, { duration: 0.5 }],
    [".b", { x: 100 }, { duration: 0.5 }],
  ],
  { repeat: 1, delay: 0.2 },
);

console.log(controls.duration); // 2.2: two 0.5 s segments, played twice, after a 0.2 s delay

Options

stagger options
OptionTypeDefaultWhat it does
eachnumber0.1Seconds between neighbouring elements. The first argument of stagger.
from"first" | "last" | "center" | number"first"The element that starts with no delay.
startDelaynumber0Seconds added to every delay.
easeEasenoneSpreads the delays along an easing curve instead of evenly.
Segment options
OptionTypeDefaultWhat it does
atnumber | "+n" | "-n" | "<" | labelnoneWhere the segment starts. Defaults to the end of the previous segment.
type, duration, ease, ...AnimateOptionsnoneAny tween, keyframes or spring option for the whole segment.
[property]AnimateOptionsnoneOptions for one property, for example { x: { type: "spring" } }.
Sequence options
OptionTypeDefaultWhat it does
repeatnumber0How many extra times the whole timeline plays.
delaynumber0Seconds before the timeline starts.
defaultTransitionAnimateOptionsnoneOptions for segments that set none.
onComplete() => voidnoneCalled when the timeline finishes.