Skip to article

Timeline sequences

Play many animations as one timeline with animate(). Add, move and remove segments, labels, callbacks and pauses.

Timelines allow the orchestration of complex animation sequences.

Pass animate() an array of animations, and it plays them one after the other.

animate([
  ["ul", { opacity: 1 }, { duration: 0.5 }],
  ["li", { x: [-100, 0] }, { ease: "easeInOut" }]
])

The result is a timeline. You can pause it, scrub it and reverse it like any other animation. You can also change it after it starts: add, move and remove animations, labels, callbacks and pauses, even while it plays.

const timeline = animate([["ul", { opacity: 1 }]])

timeline.add(["li", { x: 0 }], "+0.2")

Motion's timelines are hardware accelerated where possible, support springs, and are 25% smaller than the GSAP equivalent.

Usage

Create a sequence

A sequence is an array of segments. Each segment takes the same arguments as animate(): a subject, its keyframes and optional transition settings.

const sequence = [
  ["ul", { opacity: 1 }, { duration: 0.5 }],
  ["li", { x: [-100, 0] }]
]

animate(sequence)

By default, each segment starts when the previous one ends.

Animatable subjects & values

A sequence can animate everything that animate() can. You can mix HTML, SVG, motion values, objects, Three.js and vgpu in the same sequence:

const color = motionValue("rgba(255, 0, 0, 1)")

const sequence = [
  ["li", { x: 100 }],
  [color, "#444"],
  [cube, { y: 1, rotateY: 180 }]
]

Three.js and vgpu animate through optional effects. Register each effect once, before the first animation:

import { animate } from "motion"
import { threeEffect } from "motion/three"

animate.addEffect(threeEffect)

After that, pass Three.js objects, materials and uniforms, and vgpu nodes, cameras, lights and uniforms, as the subject of a segment:

const sequence = [
  ["h1", { opacity: 1 }],

  // Three.js mesh, starts with the heading
  [mesh, { y: 1, rotateY: 180 }, { at: "<" }],

  // vgpu camera, starts 0.5 seconds before the previous segment ends
  [camera, { fov: 70 }, { at: "-0.5" }]
]

animate(sequence)

Transitions

Each segment accepts all animate() options (except repeatDelay and repeatType). These control the duration and other settings of that segment.

const sequence = [
  ["ul", { opacity: 1 }, { duration: 1 }]
]

Both type: "keyframes" and type: "spring" transitions are supported.

You can also override the transition for each value:

const sequence = [
  [
    "ul",
    { opacity: 1, x: 100 },
    {
      duration: 1,
      x: { duration: 2 }
    }
  ]
]

Default segment transition

The defaultTransition option sets the default transition for every segment in the sequence:

animate(sequence, {
  defaultTransition: { duration: 0.2 }
})

Set the whole duration

Motion automatically calculates the duration of a sequence from its segments. To change playback of the sequence as a whole, pass animate() options as the second argument:

animate(sequence, { duration: 10, repeat: 2 })

Setting duration stretches every segment, label and callback to fit.

Stagger

Pass stagger() to a segment's delay to stagger the elements it animates. The next segment starts after the last staggered element.

animate([
  ["ul", { opacity: 1 }],
  [
    "li",
    { x: [100, 0] },
    { delay: stagger(0.05) }
  ]
])

Schedule segments

The at option changes when a segment starts. It can be a time, a label or a time relative to the previous segment.

const sequence = [
  ["ul", { opacity: 1 }],
  ["li", { x: [-100, 0] }, { at: 1 }]
]

Specific time

Set at to a number to start a segment at that time, in seconds:

const sequence = [
  ["nav", { opacity: 1 }],

  // Starts 0.5 seconds into the sequence
  ["nav", { x: 100 }, { at: 0.5 }]
]

Relative to the end of the previous segment

Set at to a string that starts with + or - to start relative to the end of the previous segment:

const sequence = [
  ["nav", { opacity: 1 }, { duration: 1 }],

  // 0.5 seconds after the previous segment ends (1.5 seconds)
  ["nav", { x: 100 }, { at: "+0.5" }],

  // 0.2 seconds before the previous segment ends
  ["nav li", { opacity: 1 }, { at: "-0.2" }]
]

Start of the previous segment

Set at to "<" to start at the same time as the previous segment:

const sequence = [
  ["nav", { x: 100 }, { duration: 1 }],

  // Starts with the x animation
  ["li", { opacity: 1 }, { at: "<" }]
]

Relative to the start of the previous segment

Set at to a string that starts with <+ or <- to start relative to the start of the previous segment:

const sequence = [
  ["nav", { opacity: 1 }],

  // 0.5 seconds after the previous segment starts (0.5 seconds)
  ["nav", { x: 100 }, { at: "<+0.5" }],

  // 0.2 seconds before the previous segment starts (0.3 seconds)
  ["nav li", { opacity: 1 }, { at: "<-0.2" }]
]

Labels

A string in a sequence defines a label. It marks the time where it appears. Set at to the label name to start a segment at that time:

const sequence = [
  ["nav", { x: 100 }, { duration: 1 }],

  "my-label", // Defined at 1 second

  ["li", { opacity: 1 }],

  // Also starts at 1 second
  ["a", { scale: 1.2 }, { at: "my-label" }]
]

A label can also have its own at:

const sequence = [
  { name: "reveal", at: 2 },
  ["li", { opacity: 1 }, { at: "reveal" }]
]

Relative label offsets

Add a + or - time to a label name to start relative to the label:

const sequence = [
  ["nav", { x: 100 }, { duration: 1 }],
  "my-label",

  // 0.5 seconds after my-label (1.5 seconds)
  ["li", { opacity: 1 }, { at: "my-label+0.5" }],

  // 0.2 seconds before my-label (0.8 seconds)
  ["a", { scale: 1.2 }, { at: "my-label-0.2" }]
]

Nested sequences

To place a group of segments as one, pass an object with a sequence and an optional at. Times inside the nested sequence are relative to its start.

const intro = [
  ["h1", { opacity: 1 }],
  ["p", { opacity: 1 }, { at: 0.2 }]
]

animate([
  ["nav", { y: 0 }, { duration: 1 }],
  { sequence: intro, at: "<" }
])

Callbacks

Progress callbacks

A function in the place of a subject runs on every frame of its segment. By default, it receives a progress value from 0 to 1:

const sequence = [
  [(progress) => console.log(progress)]
]

You can also pass keyframes:

const sequence = [
  [(color) => console.log(color), ["#000", "#fff"]]
]

Run a function once

To run a function at one point in the timeline, use call(). Timelines can play backwards and be scrubbed, so the callback receives the playback direction: 1 forwards and -1 backwards. Use it to undo what the callback did.

const timeline = animate(sequence)

timeline.call((direction) => {
  el.classList.toggle("active", direction === 1)
}, 1)

Timeline events

animate() accepts timeline event callbacks as options:

animate(sequence, {
  onUpdate: (time) => console.log(time),
  onRepeat: () => console.log("Repeat"),
  onComplete: () => console.log("Complete"),
  onReverseComplete: () => console.log("Back at the start")
})
  • onUpdate runs on every frame while the playhead moves. It receives the time in seconds within the current repeat.

  • onRepeat runs each time the timeline starts a repeat.

  • onComplete runs when the timeline finishes. Without onReverseComplete, it also runs when the timeline finishes playing backwards.

  • onReverseComplete runs in place of onComplete when the timeline finishes playing backwards, at its start.

Change a timeline

You can add, move and remove segments, labels, callbacks and pauses at any time.

const timeline = animate([
  ["ul", { opacity: 1 }]
])

timeline
  .add("list-in")
  .add(["li", { x: [-100, 0] }])
  .call(() => console.log("Done"))

Every animate() call returns a timeline, so you can also add segments to a single animation:

const animation = animate(".box", { x: 100 })

animation.add([".box", { rotate: 90 }])

A single animation plays as a normal animation until you change it, so it costs nothing extra until then.

Start paused

Pass autoplay: false to build a timeline without playing it. This is useful when you want to build it with add() first, or control it with seek() and playTo().

const timeline = animate(sequence, { autoplay: false })

Move and remove

The segment you pass to add() is also its handle. Keep a reference to it to pass to move() or remove().

const highlight = [".card", { scale: 1.1 }]

timeline.add(highlight, "end")
timeline.move(highlight, "end-0.5")
timeline.remove(highlight)

Nest timelines

Add a timeline to another timeline to play it as part of the parent. Changes to the child update the parent.

const intro = animate([["h1", { opacity: 1 }]], { autoplay: false })
const page = animate([["nav", { y: 0 }]])

page.add(intro, "<")

Pause and play to labels

addPause() pauses the timeline when the playhead reaches a time or label. playTo() plays forwards or backwards to a time or label, then pauses there.

const timeline = animate([
  ["#step-1", { opacity: 1 }],
  "step-1",
  ["#step-2", { opacity: 1 }],
  "step-2"
], { autoplay: false })

next.addEventListener("click", () => timeline.playTo("step-2"))
back.addEventListener("click", () => timeline.playTo("step-1"))

Scroll

Timelines work with scroll(). Changes to a timeline that's attached to scroll take effect at the current scroll position.

const timeline = animate(sequence)

scroll(timeline)

scopeMotion

scopeMotion() collects every timeline that animate() creates inside it. scope.stop() stops these timelines with the rest of the scope, and scope.cancel() also restores the inline styles of the elements they animated.

const scope = scopeMotion(".hero", () => {
  animate([
    ["h1", { opacity: 1 }],
    ["p", { y: [20, 0] }, { at: "-0.2" }]
  ])
})

scope.cancel()

With a root element, selectors in the sequence resolve inside the root. The scope's default transition is merged under the sequence's defaultTransition.

scopeMotion(
  () => {
    // Every segment lasts 0.3 seconds
    animate([
      [".a", { x: 100 }],
      [".b", { x: 100 }]
    ])
  },
  { transition: { duration: 0.3 } }
)

A timeline stays in its scope after setup. Segments you add later also resolve inside the root, and scope.cancel() restores the styles they changed. This is true even when you add them from outside the scope, for example in a setTimeout. After the scope stops, changes to the timeline don't play it again.

let timeline

const scope = scopeMotion(".hero", () => {
  timeline = animate([["h1", { opacity: 1 }]])
})

// Animates .hero p, not every p on the page
timeline.add(["p", { opacity: 1 }])

In React, the useScopeMotion hook creates a scope that's cancelled when the component unmounts.

Methods

A timeline has all of the animate() playback controls, like play(), pause(), time and speed. It also has these methods. Each method that changes the timeline returns the timeline, so you can chain calls.

add(item, at?)

Adds a segment, label or timeline. at sets when it starts and replaces any at already on the segment.

timeline.add(["li", { opacity: 1 }], "+0.5")
timeline.add("outro")

call(callback, at?)

Runs callback when the playhead crosses at, in either direction. It receives the playback direction: 1 forwards and -1 backwards. Callbacks don't run when you set time or call seek().

timeline.call((direction) => console.log(direction), "outro")

A callback after the end of the timeline extends its duration.

addPause(at?, callback?)

Pauses the timeline when the playhead reaches at. callback runs when it pauses.

timeline.addPause("step-2", () => showNextButton())

move(item, at)

Changes when a segment, label, callback or timeline starts.

timeline.move(segment, 2)

remove(item)

Removes a segment, label, callback or timeline. Values that it animated go back to where they were before it.

timeline.remove(segment)

clear()

Removes everything from the timeline.

getLabelTime(name)

Returns the time of a label in seconds, or undefined if the timeline has no label with this name.

const time = timeline.getLabelTime("outro")

seek(position)

Jumps to a time in seconds, a label or a label offset.

timeline.seek("outro-0.5")

playTo(position)

Plays forwards or backwards to a time, label or label offset, then pauses.

timeline.playTo("outro")

reverse()

Reverses the playback direction and plays.

timeline.reverse()

progress

Gets and sets the playhead as a progress value, from 0 (start) to 1 (end).

timeline.progress = 0.5