Lottie Integration

Sync Lottie Player animations with Motion.page timelines for scroll-driven and triggered playback.

Lottie Integration syncs a <lottie-player> (or <dotlottie-player>) element’s frame playback with any Motion.page timeline. As the timeline progresses, the Lottie animation scrubs through its frames — making scroll-driven Lottie playback a natural fit.

Prerequisites

  1. Load the Lottie Player script on your page before Motion.page initialises:
html
<!-- @lottiefiles/lottie-player -->
<script src="https://unpkg.com/@lottiefiles/lottie-player@latest/dist/lottie-player.js"></script>
html
<!-- or dotLottie Player -->
<script src="https://unpkg.com/@dotlottie/player-component@latest/dist/dotlottie-player.js"></script>
  1. Add a <lottie-player> element to your page and give it a CSS selector you can target:
html
<lottie-player
  id="hero-lottie"
  src="/animations/hero.json"
  background="transparent"
  speed="1"
></lottie-player>
  1. Use that selector as the animation target in your Motion config.

Basic Usage

Add a lottie object to your animation config. The SDK generates an onUpdate callback that scrubs the animation’s frames as the timeline progresses.

typescript
import { Motion } from "@motion.page/sdk";

Motion("hero-anim", "#hero-lottie", {
  duration: 1,
  lottie: {
    frames: [0, 1],
  },
}).onPageLoad();

frames is a normalized range — 0 is the first frame, 1 is the last frame. [0, 1] plays the full animation.

Note: lottie is a root-level property on the animation config — it is not placed inside from or to. Standard from/to properties are not needed for Lottie animations; duration controls the timeline length used for scrubbing.


Options

OptionTypeDefaultDescription
frames[number, number][0, 1]Normalized frame range. 0 = first frame, 1 = last frame
reversebooleanfalsePlay the frame range in reverse as the timeline progresses

Scroll-Synced Playback

The most common pattern: scrub the Lottie animation as the user scrolls. As scroll progress goes from 0 → 1, the animation plays from its first frame to its last.

lottie-scroll.ts
Motion("scroll-lottie", "#hero-lottie", {
duration: 1,
lottie: {
  frames: [0, 1],
},
}).onScroll({ scrub: true });

Control the scroll range using start and end:

typescript
Motion("scroll-lottie", "#hero-lottie", {
  duration: 1,
  lottie: {
    frames: [0, 1],
  },
}).onScroll({
  scrub: true,
  start: "top center",
  end: "bottom center",
});

Partial Frame Range

Use a sub-range of frames instead of the full animation. Values are normalized 0–1, where 0.25 means 25% through the total frame count.

typescript
// Only play the middle half of the animation (frames 25%–75%)
Motion("partial-lottie", "#feature-lottie", {
  duration: 1,
  lottie: {
    frames: [0.25, 0.75],
  },
}).onScroll({ scrub: true });

Reverse Playback

Set reverse: true to play frames in reverse direction as the timeline progresses forward. The frame range is preserved — only the direction is swapped.

typescript
// Plays from last frame to first frame as scroll advances
Motion("reverse-lottie", "#icon-lottie", {
  duration: 1,
  lottie: {
    frames: [0, 1],
    reverse: true,
  },
}).onScroll({ scrub: true });
framesreverseResult
[0, 1]falseFirst frame → last frame
[0, 1]trueLast frame → first frame
[0.25, 0.75]false25% frame → 75% frame
[0.25, 0.75]true75% frame → 25% frame

Triggered Playback

Lottie animations work with any Motion.page trigger — not just scroll. Use .onPageLoad(), .onHover(), or .onClick() to drive the timeline (and therefore the Lottie frames) with those interactions.

typescript
// Play through all frames once on page load
Motion("load-lottie", "#intro-lottie", {
  duration: 2,
  lottie: {
    frames: [0, 1],
  },
}).onPageLoad();
typescript
// Scrub through frames on hover, reverse on leave
Motion("hover-lottie", ".card-lottie", {
  duration: 0.6,
  lottie: {
    frames: [0, 1],
  },
}).onHover({ each: true, onLeave: "reverse" });

How It Works

The integration bridges Motion.page timelines to lottie-web’s API. The SDK generates an onUpdate callback attached to the timeline. On every tick, it:

  1. Reads el._lottie.totalFrames — lottie-web exposes its AnimationItem directly on the DOM element as ._lottie
  2. Calculates the target frame: startFrame + (endFrame - startFrame) * progress
  3. Calls el._lottie.goToAndStop(frameIndex, true) to seek without playing

The equivalent manual implementation for a full-range scrub:

typescript
Motion("manual-lottie", "#hero-lottie", {
  duration: 1,
  onUpdate: (p) => {
    document.querySelectorAll("#hero-lottie").forEach((el) => {
      if (el._lottie) {
        const t = el._lottie.totalFrames - 1;
        el._lottie.goToAndStop(Math.round(p * t), true);
      }
    });
  },
}).onScroll({ scrub: true });

Using the lottie config option generates this callback automatically.


Limitations

Lottie and Timeline Control’s onUpdate cannot be combined. Both use the timeline’s .onUpdate() slot. If a Lottie animation is present in a timeline, any custom onUpdate set via Timeline Control is suppressed for that timeline.

See Timeline Control for details on the onUpdate slot.


Common Patterns

Full-Page Scroll Story

Pin a section and scrub the Lottie through its full animation while the user scrolls through the pinned section.

typescript
Motion("story-lottie", "#story-lottie", {
  duration: 1,
  lottie: {
    frames: [0, 1],
  },
}).onScroll({
  scrub: true,
  pin: true,
  start: "top top",
  end: "+=200%",
});

Multiple Lottie Sections

Each section plays a different frame range of the same animation — useful for step-by-step product demos.

typescript
// Step 1: frames 0–33%
Motion("step-1", "#product-lottie", {
  duration: 1,
  lottie: { frames: [0, 0.33] },
}).onScroll({ scrub: true, start: "top 80%", end: "center center" });

// Step 2: frames 33–66%
Motion("step-2", "#product-lottie", {
  duration: 1,
  lottie: { frames: [0.33, 0.66] },
}).onScroll({ scrub: true, start: "center center", end: "bottom center" });

Lottie Icon on Hover

Animate a Lottie icon through a short loop on hover.

typescript
Motion("icon-hover", ".nav-icon lottie-player", {
  duration: 0.5,
  lottie: {
    frames: [0, 1],
  },
}).onHover({ each: true });

See Also