Lottie Integration

Sync Lottie Player frames with Motion.page timeline progress.

Lottie playback can follow any Motion.page timeline by mapping AnimationConfig.onUpdate progress to the player’s frame API. This makes page-load, hover, click, and scroll-scrub timelines available to Lottie animations without starting a second playback clock.

lottie is Builder metadata, not a property of the core SDK’s AnimationConfig. The Builder’s SDK generator compiles that metadata into the same supported onUpdate bridge shown on this page.

Prerequisites

Load the Lottie Player component before initializing the timeline:

html
<script src="https://unpkg.com/@lottiefiles/lottie-player@latest/dist/lottie-player.js"></script>

Then add a player without autoplay or loop; Motion.page will own its frame position:

html
<lottie-player
  id="hero-lottie"
  src="/animations/hero.json"
  background="transparent"
  style="width: 320px; height: 320px;"
></lottie-player>

The LottieFiles player exposes its lottie-web AnimationItem as element._lottie. That object provides totalFrames and goToAndStop().

Basic Usage

Create a small seek helper, then call it from onUpdate:

lottie-page-load.ts
import { Motion } from "@motion.page/sdk";

type LottieElement = HTMLElement & {
_lottie?: {
  totalFrames: number;
  goToAndStop(frame: number, isFrame: boolean): void;
};
};

const player = document.querySelector<LottieElement>("#hero-lottie")!;

function seekLottie(
element: LottieElement,
progress: number,
start = 0,
end = 1
) {
const animation = element._lottie;
if (!animation) return;

const lastFrame = animation.totalFrames - 1;
const normalized = start + (end - start) * progress;
animation.goToAndStop(Math.round(normalized * lastFrame), true);
}

Motion("hero-lottie", player, {
duration: 2,
ease: "none",
onUpdate: (progress) => seekLottie(player, progress),
}).onPageLoad();

The preview uses a tiny local player stub so it stays self-contained. Its totalFrames and goToAndStop() contract is the same one used by the LottieFiles player and by Builder-generated code.

Scroll-Synced Playback

Attach .onScroll({ scrub: true }) to map scroll position directly to the frame range:

typescript
Motion("scroll-lottie", player, {
  duration: 1,
  ease: "none",
  onUpdate: (progress) => seekLottie(player, progress),
}).onScroll({
  target: "#hero-lottie",
  start: "top center",
  end: "bottom center",
  scrub: true,
});

Scrolling backward naturally sends decreasing progress values, so the Lottie follows in reverse without additional event handling.

Partial Frame Range

Pass normalized start and end values to the helper. This plays the middle half of the animation:

typescript
Motion("partial-lottie", player, {
  duration: 1,
  ease: "none",
  onUpdate: (progress) => seekLottie(player, progress, 0.25, 0.75),
}).onScroll({ target: "#hero-lottie", scrub: true });
Start End Result
0 1 First frame → last frame
0.25 0.75 25% frame → 75% frame
1 0 Last frame → first frame
0.75 0.25 75% frame → 25% frame

Reversing is simply a range whose start is greater than its end; no separate SDK option is required.

Hover and Click

The same frame bridge works with interaction triggers:

typescript
Motion("lottie-hover", ".card-lottie", {
  duration: 0.7,
  ease: "none",
  onUpdate: (progress) => {
    document.querySelectorAll<LottieElement>(".card-lottie").forEach((element) => {
      seekLottie(element, progress);
    });
  },
}).onHover({ each: true, onLeave: "reverse" });

For independent players, keep each: true so hovering one card does not drive every card’s timeline instance.

Builder-Generated Bridge

When Lottie is enabled in the Builder, its SDK generator converts the normalized frame range and reverse toggle into an animation-level callback equivalent to:

typescript
onUpdate: (progress) => {
  document.querySelectorAll("#hero-lottie").forEach((element) => {
    if (!element._lottie) return;

    const lastFrame = element._lottie.totalFrames - 1;
    const frame = Math.round(0 * lastFrame + (1 - 0) * lastFrame * progress);
    element._lottie.goToAndStop(frame, true);
  });
}

This callback is generated from Builder metadata. Handwritten core SDK code should use onUpdate directly rather than adding a root-level lottie object.

Limitations

  • Wait for the player to initialize. The guarded helper safely skips ticks until element._lottie exists.
  • Use one animation callback per player target. An AnimationConfig has one onUpdate function; combine additional progress-driven work inside that function.
  • Do not enable autoplay. A player running on its own clock will fight the timeline’s frame seeking.
  • Refresh scroll positions after layout shifts. Call Motion.refreshScrollTriggers() if loading the player changes document geometry after trigger creation.