Lottie
Render After Effects animations on the web with lottie-react and lottie-web — player controls, interactivity, lazy loading, and the light player for optimized bundles.
Lottie bridges the gap between motion designers and developers. Designers export animations from After Effects (via the Bodymovin plugin) as JSON files, and Lottie renders them natively in the browser using SVG, Canvas, or HTML. The result is resolution-independent, scriptable animation without hand-coding keyframes. ## Key Points - **Use the light player when possible.** The full lottie-web build includes Canvas and HTML renderers most projects never use. The light SVG-only build saves ~180KB. - **Lazy-load animation JSON.** Inline `import animData from "./anim.json"` bundles the JSON into your JS. For large files (>50KB), fetch them asynchronously. - **Pause off-screen animations.** Use IntersectionObserver to pause Lottie instances that are not visible. Each running animation consumes CPU for frame interpolation. - **Prefer SVG renderer for quality, Canvas for performance.** SVG scales perfectly but the DOM node count grows with complexity. Canvas is faster for particle-heavy animations. - **Optimize JSON files with LottieFiles.** The LottieFiles editor can strip unused layers, reduce precision, and compress path data without visible quality loss. - **Destroy animations on unmount.** Failing to call `destroy()` on lottie-web instances causes memory leaks. The `lottie-react` wrapper handles this automatically. - **Keep animation files under 100KB.** Large Lottie files with many layers and effects can cause jank. Simplify in After Effects before exporting. - **Bundling animation JSON as static imports for large files.** This bloats your JavaScript bundle. Fetch them at runtime or use dynamic `import()` with code splitting. - **Running multiple looping Lottie instances simultaneously.** Each instance runs its own requestAnimationFrame loop. More than 3-4 concurrent animations can drop frame rates on mobile. - **Using Lottie for simple CSS-achievable animations.** A fade-in or slide does not need a 30KB JSON file. Reserve Lottie for complex motion that would be impractical to code by hand. - **Ignoring `destroy()` in vanilla JS.** Without cleanup, lottie-web keeps references to DOM nodes and continues its animation loop after the container is removed. - **Not setting dimensions on the container.** Without explicit width/height, Lottie animations can cause layout shifts as they load and determine their intrinsic size. ## Quick Example ```bash npm install lottie-react ``` ```bash npm install lottie-web ```
skilldb get animation-services-skills/LottieFull skill: 329 linesLottie
Core Philosophy
Lottie bridges the gap between motion designers and developers. Designers export animations from After Effects (via the Bodymovin plugin) as JSON files, and Lottie renders them natively in the browser using SVG, Canvas, or HTML. The result is resolution-independent, scriptable animation without hand-coding keyframes.
Key mental model: the animation is an asset, not code. Developers control playback (play, pause, seek, loop, speed) but the motion itself lives in the JSON file, authored in After Effects or Lottie-compatible tools like LottieFiles, Haiku, or Rive's Lottie export.
Setup
lottie-react (Recommended for React)
npm install lottie-react
import Lottie from "lottie-react";
import checkmarkAnimation from "./animations/checkmark.json";
export function SuccessIndicator() {
return (
<Lottie
animationData={checkmarkAnimation}
loop={false}
autoplay={true}
style={{ width: 120, height: 120 }}
/>
);
}
lottie-web (Framework-agnostic)
npm install lottie-web
import { useRef, useEffect } from "react";
import lottie, { AnimationItem } from "lottie-web";
export function LottiePlayer({ animationPath }: { animationPath: string }) {
const containerRef = useRef<HTMLDivElement>(null);
const animRef = useRef<AnimationItem | null>(null);
useEffect(() => {
if (!containerRef.current) return;
animRef.current = lottie.loadAnimation({
container: containerRef.current,
renderer: "svg",
loop: true,
autoplay: true,
path: animationPath,
});
return () => {
animRef.current?.destroy();
};
}, [animationPath]);
return <div ref={containerRef} style={{ width: 200, height: 200 }} />;
}
Light Player (Reduced Bundle)
The default lottie-web bundle is ~250KB. The light build drops Canvas/HTML renderers and expression support, coming in at ~70KB.
import lottie from "lottie-web/build/player/lottie_light";
Use the light player when you only need SVG rendering and your animations do not use After Effects expressions.
Key Techniques
Player Controls
import { useRef } from "react";
import Lottie, { LottieRefCurrentProps } from "lottie-react";
import loadingAnimation from "./animations/loading.json";
export function ControllableAnimation() {
const lottieRef = useRef<LottieRefCurrentProps>(null);
const handlePlay = () => lottieRef.current?.play();
const handlePause = () => lottieRef.current?.pause();
const handleStop = () => lottieRef.current?.stop();
const handleSpeed = (speed: number) => lottieRef.current?.setSpeed(speed);
const handleSeek = (frame: number) => lottieRef.current?.goToAndStop(frame, true);
return (
<div>
<Lottie
lottieRef={lottieRef}
animationData={loadingAnimation}
loop={true}
autoplay={false}
style={{ width: 200, height: 200 }}
/>
<div className="flex gap-2 mt-4">
<button onClick={handlePlay}>Play</button>
<button onClick={handlePause}>Pause</button>
<button onClick={handleStop}>Stop</button>
<button onClick={() => handleSpeed(2)}>2x Speed</button>
<button onClick={() => handleSeek(30)}>Go to frame 30</button>
</div>
</div>
);
}
Event Handling
export function AnimationWithEvents() {
const handleComplete = () => {
console.log("Animation completed");
};
const handleLoopComplete = () => {
console.log("Loop iteration finished");
};
const handleEnterFrame = (event: { currentTime: number; totalTime: number }) => {
// Fires on every frame — use sparingly
};
return (
<Lottie
animationData={animationData}
loop={true}
onComplete={handleComplete}
onLoopComplete={handleLoopComplete}
onEnterFrame={handleEnterFrame}
/>
);
}
Scroll-Linked Playback
Tie animation progress to scroll position for storytelling sections.
import { useRef, useEffect, useState } from "react";
import Lottie, { LottieRefCurrentProps } from "lottie-react";
import scrollAnimation from "./animations/scroll-story.json";
export function ScrollLinkedLottie() {
const lottieRef = useRef<LottieRefCurrentProps>(null);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleScroll = () => {
if (!containerRef.current || !lottieRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const scrollProgress = Math.max(0, Math.min(1,
(window.innerHeight - rect.top) / (window.innerHeight + rect.height)
));
const totalFrames = lottieRef.current.getDuration(true) ?? 0;
const targetFrame = Math.floor(scrollProgress * totalFrames);
lottieRef.current.goToAndStop(targetFrame, true);
};
window.addEventListener("scroll", handleScroll, { passive: true });
return () => window.removeEventListener("scroll", handleScroll);
}, []);
return (
<div ref={containerRef} style={{ height: "200vh", position: "relative" }}>
<div style={{ position: "sticky", top: 0, height: "100vh" }}>
<Lottie
lottieRef={lottieRef}
animationData={scrollAnimation}
autoplay={false}
loop={false}
style={{ width: "100%", maxWidth: 600, margin: "0 auto" }}
/>
</div>
</div>
);
}
Lazy Loading Animations
Large animation JSON files should be loaded on demand to avoid blocking initial page load.
import { lazy, Suspense, useEffect, useState } from "react";
import Lottie from "lottie-react";
export function LazyLottie({ animationPath }: { animationPath: string }) {
const [animationData, setAnimationData] = useState<object | null>(null);
useEffect(() => {
let cancelled = false;
fetch(animationPath)
.then((res) => res.json())
.then((data) => {
if (!cancelled) setAnimationData(data);
});
return () => { cancelled = true; };
}, [animationPath]);
if (!animationData) {
return <div className="w-48 h-48 bg-gray-100 animate-pulse rounded" />;
}
return (
<Lottie
animationData={animationData}
loop={true}
autoplay={true}
style={{ width: 192, height: 192 }}
/>
);
}
Intersection Observer for Play-on-View
import { useRef, useState, useEffect } from "react";
import Lottie, { LottieRefCurrentProps } from "lottie-react";
import animationData from "./animations/feature.json";
export function PlayOnView() {
const lottieRef = useRef<LottieRefCurrentProps>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [isInView, setIsInView] = useState(false);
useEffect(() => {
if (!containerRef.current) return;
const observer = new IntersectionObserver(
([entry]) => {
setIsInView(entry.isIntersecting);
if (entry.isIntersecting) {
lottieRef.current?.play();
} else {
lottieRef.current?.pause();
}
},
{ threshold: 0.3 }
);
observer.observe(containerRef.current);
return () => observer.disconnect();
}, []);
return (
<div ref={containerRef}>
<Lottie
lottieRef={lottieRef}
animationData={animationData}
autoplay={false}
loop={true}
/>
</div>
);
}
Interactive Hover Animation
import { useRef } from "react";
import Lottie, { LottieRefCurrentProps } from "lottie-react";
import hoverAnimation from "./animations/hover-icon.json";
export function HoverLottie() {
const lottieRef = useRef<LottieRefCurrentProps>(null);
return (
<div
onMouseEnter={() => {
lottieRef.current?.setDirection(1);
lottieRef.current?.play();
}}
onMouseLeave={() => {
lottieRef.current?.setDirection(-1);
lottieRef.current?.play();
}}
>
<Lottie
lottieRef={lottieRef}
animationData={hoverAnimation}
autoplay={false}
loop={false}
style={{ width: 64, height: 64 }}
/>
</div>
);
}
Best Practices
- Use the light player when possible. The full lottie-web build includes Canvas and HTML renderers most projects never use. The light SVG-only build saves ~180KB.
- Lazy-load animation JSON. Inline
import animData from "./anim.json"bundles the JSON into your JS. For large files (>50KB), fetch them asynchronously. - Pause off-screen animations. Use IntersectionObserver to pause Lottie instances that are not visible. Each running animation consumes CPU for frame interpolation.
- Prefer SVG renderer for quality, Canvas for performance. SVG scales perfectly but the DOM node count grows with complexity. Canvas is faster for particle-heavy animations.
- Optimize JSON files with LottieFiles. The LottieFiles editor can strip unused layers, reduce precision, and compress path data without visible quality loss.
- Destroy animations on unmount. Failing to call
destroy()on lottie-web instances causes memory leaks. Thelottie-reactwrapper handles this automatically. - Keep animation files under 100KB. Large Lottie files with many layers and effects can cause jank. Simplify in After Effects before exporting.
Anti-Patterns
- Bundling animation JSON as static imports for large files. This bloats your JavaScript bundle. Fetch them at runtime or use dynamic
import()with code splitting. - Running multiple looping Lottie instances simultaneously. Each instance runs its own requestAnimationFrame loop. More than 3-4 concurrent animations can drop frame rates on mobile.
- Using Lottie for simple CSS-achievable animations. A fade-in or slide does not need a 30KB JSON file. Reserve Lottie for complex motion that would be impractical to code by hand.
- Ignoring
destroy()in vanilla JS. Without cleanup, lottie-web keeps references to DOM nodes and continues its animation loop after the container is removed. - Not setting dimensions on the container. Without explicit width/height, Lottie animations can cause layout shifts as they load and determine their intrinsic size.
- Using After Effects expressions with the light player. The light build strips expression support. Animations that rely on expressions will render incorrectly or not at all.
Install this skill directly: skilldb add animation-services-skills
Related Skills
Anime.js
Lightweight JavaScript animation engine — DOM, CSS, SVG, and object property animations with timeline sequencing, staggering, and spring-based easing.
Auto Animate
Zero-config animation library by FormKit — automatic transitions for DOM additions, removals, and reordering with a single function call or directive.
Framer Motion (Motion)
Production-grade animation library for React — animate, variants, AnimatePresence, layout animations, gestures, scroll-triggered effects, useMotionValue, and stagger orchestration.
GSAP
GreenSock Animation Platform — timeline orchestration, ScrollTrigger, tweens, stagger, React integration with useGSAP, SplitText, morphSVG, and from/to/fromTo patterns.
Motion One
Lightweight Web Animations API wrapper — animate, timeline, spring physics, scroll-linked animations, stagger, and minimal bundle size for modern browsers.
Popmotion
Functional animation library — spring physics, decay, keyframe interpolation, and composable animation primitives that power Framer Motion's internals.