Fix: Lottie Not Working — Animation Not Playing, File Not Loading, or React Component Blank
Quick Answer
How to fix Lottie animation issues — lottie-react and lottie-web setup, JSON animation loading, playback control, interactivity, lazy loading, and performance optimization.
The Problem
The Lottie animation component renders but shows nothing:
import Lottie from 'lottie-react';
import animationData from './animation.json';
function MyAnimation() {
return <Lottie animationData={animationData} />;
// Empty div — no animation visible
}Or the animation file fails to load:
Error: Cannot find module './animation.json'
// Or: Failed to parse JSONOr the animation plays once and stops:
Animation runs through once then disappearsWhy This Happens
Lottie renders After Effects animations exported as JSON via the Bodymovin plugin. The React wrapper has specific requirements:
- The animation JSON must be valid Lottie format — not every JSON file is a Lottie animation. It must be exported from After Effects using Bodymovin or created with tools like LottieFiles. Invalid JSON or a non-Lottie file renders nothing.
- The container needs dimensions — Lottie fills its parent container. If the parent has zero width or height, the animation is invisible. Either set dimensions on the Lottie component or on its parent.
loopdefaults totrueinlottie-reactbutfalseinlottie-web— depending on which library you use, the default loop behavior differs. An animation that plays once and stops hasloop: false.- Large Lottie files impact performance — complex animations with many layers, expressions, or embedded images create large JSON files that slow down parsing and rendering. Lazy loading is important for performance.
Fix 1: Basic Setup with lottie-react
npm install lottie-react
# Or for lower-level control:
# npm install lottie-web'use client';
import Lottie from 'lottie-react';
import loadingAnimation from '@/animations/loading.json';
// Basic usage — plays automatically, loops by default
function LoadingSpinner() {
return (
<Lottie
animationData={loadingAnimation}
loop={true}
style={{ width: 200, height: 200 }} // Set explicit size
/>
);
}
// With playback control
import { useRef } from 'react';
import type { LottieRefCurrentProps } from 'lottie-react';
function ControlledAnimation() {
const lottieRef = useRef<LottieRefCurrentProps>(null);
return (
<div>
<Lottie
lottieRef={lottieRef}
animationData={loadingAnimation}
loop={false}
autoplay={false} // Don't play on mount
style={{ width: 300, height: 300 }}
/>
<div>
<button onClick={() => lottieRef.current?.play()}>Play</button>
<button onClick={() => lottieRef.current?.pause()}>Pause</button>
<button onClick={() => lottieRef.current?.stop()}>Stop</button>
<button onClick={() => {
lottieRef.current?.goToAndStop(0, true); // Reset to first frame
lottieRef.current?.play();
}}>
Replay
</button>
<button onClick={() => lottieRef.current?.setSpeed(2)}>2x Speed</button>
<button onClick={() => lottieRef.current?.setDirection(-1)}>Reverse</button>
</div>
</div>
);
}Fix 2: Load Animation from URL
'use client';
import Lottie from 'lottie-react';
import { useEffect, useState } from 'react';
// Load from URL instead of importing JSON
function RemoteAnimation({ url }: { url: string }) {
const [animationData, setAnimationData] = useState<object | null>(null);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(setAnimationData)
.catch(err => console.error('Failed to load animation:', err));
}, [url]);
if (!animationData) return <div>Loading...</div>;
return (
<Lottie
animationData={animationData}
loop
style={{ width: 300, height: 300 }}
/>
);
}
// Usage
<RemoteAnimation url="https://lottie.host/xxx/animation.json" />
<RemoteAnimation url="/animations/success.json" />Fix 3: Interactive Animations (Hover, Scroll, Click)
'use client';
import Lottie from 'lottie-react';
import { useRef, useState } from 'react';
import type { LottieRefCurrentProps } from 'lottie-react';
import heartAnimation from '@/animations/heart.json';
// Play on hover
function HoverAnimation() {
const lottieRef = useRef<LottieRefCurrentProps>(null);
return (
<div
onMouseEnter={() => lottieRef.current?.play()}
onMouseLeave={() => {
lottieRef.current?.goToAndStop(0, true);
}}
>
<Lottie
lottieRef={lottieRef}
animationData={heartAnimation}
loop={false}
autoplay={false}
style={{ width: 80, height: 80, cursor: 'pointer' }}
/>
</div>
);
}
// Toggle animation (like button)
function LikeButton() {
const lottieRef = useRef<LottieRefCurrentProps>(null);
const [liked, setLiked] = useState(false);
function handleClick() {
if (liked) {
lottieRef.current?.goToAndStop(0, true);
} else {
lottieRef.current?.goToAndPlay(0, true);
}
setLiked(!liked);
}
return (
<button onClick={handleClick} style={{ background: 'none', border: 'none' }}>
<Lottie
lottieRef={lottieRef}
animationData={heartAnimation}
loop={false}
autoplay={false}
style={{ width: 60, height: 60 }}
/>
</button>
);
}
// Scroll-driven animation
function ScrollLottie() {
const containerRef = useRef<HTMLDivElement>(null);
const lottieRef = useRef<LottieRefCurrentProps>(null);
useEffect(() => {
function 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;
lottieRef.current.goToAndStop(scrollProgress * totalFrames, true);
}
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return (
<div ref={containerRef} style={{ height: '200vh' }}>
<div style={{ position: 'sticky', top: '20%' }}>
<Lottie
lottieRef={lottieRef}
animationData={animationData}
autoplay={false}
loop={false}
style={{ width: 400, height: 400 }}
/>
</div>
</div>
);
}Fix 4: Using lottie-web Directly
'use client';
import lottie, { type AnimationItem } from 'lottie-web';
import { useEffect, useRef } from 'react';
function LottieWeb({ path, loop = true, autoplay = true }: {
path: string;
loop?: boolean;
autoplay?: boolean;
}) {
const containerRef = useRef<HTMLDivElement>(null);
const animationRef = useRef<AnimationItem | null>(null);
useEffect(() => {
if (!containerRef.current) return;
animationRef.current = lottie.loadAnimation({
container: containerRef.current,
renderer: 'svg', // 'svg' | 'canvas' | 'html'
loop,
autoplay,
path, // URL to JSON file
// Or: animationData: jsonObject,
});
// Events
animationRef.current.addEventListener('complete', () => {
console.log('Animation completed');
});
animationRef.current.addEventListener('loopComplete', () => {
console.log('Loop completed');
});
return () => {
animationRef.current?.destroy();
};
}, [path, loop, autoplay]);
return <div ref={containerRef} style={{ width: 300, height: 300 }} />;
}
// Use canvas renderer for better performance
lottie.loadAnimation({
container: element,
renderer: 'canvas', // Faster for complex animations
loop: true,
autoplay: true,
animationData: data,
rendererSettings: {
preserveAspectRatio: 'xMidYMid slice',
clearCanvas: true,
progressiveLoad: true,
},
});Fix 5: dotLottie (Smaller Files)
npm install @lottiefiles/dotlottie-react'use client';
import { DotLottieReact } from '@lottiefiles/dotlottie-react';
// .lottie files are compressed — much smaller than .json
function DotLottieAnimation() {
return (
<DotLottieReact
src="/animations/loading.lottie" // .lottie format
loop
autoplay
style={{ width: 300, height: 300 }}
/>
);
}
// From LottieFiles URL
function RemoteDotLottie() {
return (
<DotLottieReact
src="https://lottie.host/xxx/animation.lottie"
loop
autoplay
/>
);
}Fix 6: Performance and Lazy Loading
'use client';
import dynamic from 'next/dynamic';
import { Suspense, lazy } from 'react';
// Lazy load Lottie component — don't include in initial bundle
const Lottie = dynamic(() => import('lottie-react'), { ssr: false });
function LazyAnimation() {
const [animationData, setAnimationData] = useState(null);
useEffect(() => {
// Load animation data only when component mounts
import('@/animations/hero.json').then(mod => setAnimationData(mod.default));
}, []);
if (!animationData) return <div style={{ width: 400, height: 400 }} />;
return (
<Lottie
animationData={animationData}
loop
style={{ width: 400, height: 400 }}
/>
);
}
// Intersection Observer — only load when visible
function LazyVisibleAnimation({ src }: { src: string }) {
const ref = useRef<HTMLDivElement>(null);
const [isVisible, setIsVisible] = useState(false);
const [data, setData] = useState(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
},
{ rootMargin: '200px' },
);
if (ref.current) observer.observe(ref.current);
return () => observer.disconnect();
}, []);
useEffect(() => {
if (isVisible) {
fetch(src).then(r => r.json()).then(setData);
}
}, [isVisible, src]);
return (
<div ref={ref} style={{ width: 300, height: 300 }}>
{data && <Lottie animationData={data} loop />}
</div>
);
}Still Not Working?
Animation renders as empty div — the container has zero dimensions. Set style={{ width: 300, height: 300 }} on the Lottie component, or ensure the parent container has defined dimensions. Lottie SVGs fill the container.
JSON file not found or parse error — place animation files in public/animations/ for URL loading, or in src/animations/ for import. Files in public/ are served as-is at /animations/file.json. Imported JSON files are bundled into your JavaScript.
Animation plays once and stops — set loop={true}. In lottie-react, loop defaults to true, but if you’ve set loop={false} or are using lottie-web directly (which defaults to false), the animation won’t repeat.
Large animation causes jank — complex Lottie files can have hundreds of layers. Use the canvas renderer instead of SVG for better performance: renderer: 'canvas' in lottie-web. Reduce the animation’s dimensions and complexity in After Effects. Consider .lottie format which is compressed.
For related animation issues, see Fix: GSAP Not Working and Fix: Framer Motion Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: AutoAnimate Not Working — Transitions Not Playing, List Items Not Animating, or React State Changes Ignored
How to fix @formkit/auto-animate issues — parent ref setup, React useAutoAnimate hook, Vue directive, animation customization, disabling for specific elements, and framework integration.
Fix: Blurhash Not Working — Placeholder Not Rendering, Encoding Failing, or Colors Wrong
How to fix Blurhash image placeholder issues — encoding with Sharp, decoding in React, canvas rendering, Next.js image placeholders, CSS blur fallback, and performance optimization.
Fix: Embla Carousel Not Working — Slides Not Scrolling, Autoplay Not Starting, or Thumbnails Not Syncing
How to fix Embla Carousel issues — React setup, slide sizing, autoplay and navigation plugins, loop mode, thumbnail carousels, responsive breakpoints, and vertical scrolling.
Fix: GSAP Not Working — Animations Not Playing, ScrollTrigger Not Firing, or React Cleanup Issues
How to fix GSAP animation issues — timeline and tween basics, ScrollTrigger setup, React useGSAP hook, cleanup and context, SplitText, stagger animations, and Next.js integration.