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.
lottieis Builder metadata, not a property of the core SDK’sAnimationConfig. The Builder’s SDK generator compiles that metadata into the same supportedonUpdatebridge shown on this page.
Prerequisites
Load the Lottie Player component before initializing the timeline:
<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:
<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:
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:
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:
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:
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:
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._lottieexists. - Use one animation callback per player target. An
AnimationConfighas oneonUpdatefunction; 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.
Related
- Scroll Trigger — drive frames with scroll position
- Scroll Trigger Advanced — pin and scrub a full-page Lottie story
- Timeline Control — react to timeline-level progress and lifecycle events