How to Use Lottie in React — Complete Guide with Code
How to use Lottie animations in React with lottie-react and lottie-web. Covers installation, JSON import, play/pause control, hover triggers, and common pitfalls.
Lottie animations are JSON files exported from After Effects (or purpose-built tools). In React, you render them with the lottie-react package — a thin wrapper around the lottie-web runtime. This guide covers the complete setup, from install to production-ready patterns.
Download Lottie JSON files at Unicorn Icons — 100 are free, no account needed.
lottie-react vs lottie-web — which to use?
lottie-web is the core rendering library. lottie-react wraps it with a declarative component API.
Use lottie-react when:
- You want props-driven control (
loop,autoplay,speed) - You have a standard React component tree
Use lottie-web directly when:
- You need fine-grained imperative control
- You're integrating with a non-React UI layer
- You want to minimize dependencies
This guide covers lottie-react. For lottie-web direct usage, see the HTML guide.
Setup
1. Install
bashcopynpm install lottie-react # or yarn add lottie-react
lottie-react includes lottie-web as a peer dependency — no separate install needed.
2. Basic usage
tsxcopyimport Lottie from "lottie-react"; import arrowAnimation from "./arrow.json"; export default function ArrowIcon() { return ( <Lottie animationData={arrowAnimation} loop={true} style={{ width: 32, height: 32 }} /> ); }
Three things to note:
animationDatatakes the parsed JSON object — not a URL stringloop={true}repeats indefinitely;loop={false}plays once- Always set an explicit width/height — the canvas needs dimensions to render
3. Controlling playback
For hover animations, button toggles, or scroll triggers, use lottieRef:
tsxcopyimport Lottie, { LottieRefCurrentProps } from "lottie-react"; import { useRef } from "react"; import checkAnimation from "./check.json"; export default function CheckIcon() { const lottieRef = useRef<LottieRefCurrentProps>(null); return ( <button onMouseEnter={() => lottieRef.current?.play()} onMouseLeave={() => lottieRef.current?.stop()} > <Lottie lottieRef={lottieRef} animationData={checkAnimation} loop={false} autoplay={false} style={{ width: 32, height: 32 }} /> </button> ); }
lottieRef.current exposes: play(), pause(), stop(), setSpeed(n), setDirection(1 | -1), goToAndPlay(frame), goToAndStop(frame).
4. Playing once and stopping at last frame
tsxcopy<Lottie animationData={animation} loop={false} onComplete={() => lottieRef.current?.goToAndStop( lottieRef.current.getDuration(true) - 1, true )} />
getDuration(true) returns total frames. goToAndStop(frame, true) holds the last frame so the icon doesn't snap back to frame 0.
Common pitfalls
Pitfall 1: Animation JSON changes on every render
tsxcopy// ❌ Creates a new object reference every render <Lottie animationData={{ v: "5.9.0", ... }} /> // ✅ Import at module level import animationData from "./animation.json"; <Lottie animationData={animationData} />
Inline JSON objects cause the animation to restart on every parent re-render. Always import JSON files at module level or define them outside the component.
Pitfall 2: No width/height → invisible or wrong size
tsxcopy// ❌ Canvas has no dimensions <Lottie animationData={animation} /> // ✅ Always set explicit dimensions <Lottie animationData={animation} style={{ width: 32, height: 32 }} /> // or with Tailwind: <div className="w-8 h-8"> <Lottie animationData={animation} /> </div>
Pitfall 3: Forgetting autoplay={false} for interactive icons
tsxcopy// ❌ Animation plays on mount and the ref.play() call on hover is out of sync <Lottie lottieRef={ref} animationData={animation} /> // ✅ <Lottie lottieRef={ref} animationData={animation} autoplay={false} />
Pitfall 4: SSR in Next.js (document is not defined)
lottie-web uses document and window — it can't run server-side. In Next.js:
tsxcopy// components/LottieIcon.tsx "use client"; import Lottie from "lottie-react"; // ... rest of component
Or with dynamic import:
tsxcopyimport dynamic from "next/dynamic"; const Lottie = dynamic(() => import("lottie-react"), { ssr: false });
See the Next.js animated icons guide for a full walkthrough.
Performance
lottie-web is ~250KB minified. For apps with many icons, strategies to keep bundle size under control:
1. Load JSON on demand:
tsxcopyimport { useState, useEffect } from "react"; import Lottie from "lottie-react"; export default function LazyLottie({ src }: { src: string }) { const [data, setData] = useState<object | null>(null); useEffect(() => { fetch(src).then(r => r.json()).then(setData); }, [src]); if (!data) return <div style={{ width: 32, height: 32 }} />; return <Lottie animationData={data} style={{ width: 32, height: 32 }} />; }
2. Use renderer: "svg" for icons (default is "svg" — no change needed).
3. Avoid rendering off-screen animations. Use an Intersection Observer to pause() icons that aren't visible.
Full reusable component
A production-ready LottieIcon component:
tsxcopy"use client"; import Lottie, { LottieRefCurrentProps } from "lottie-react"; import { useRef, useCallback } from "react"; interface LottieIconProps { animationData: object; size?: number; playOnHover?: boolean; loop?: boolean; className?: string; } export default function LottieIcon({ animationData, size = 32, playOnHover = false, loop = true, className, }: LottieIconProps) { const lottieRef = useRef<LottieRefCurrentProps>(null); const handleMouseEnter = useCallback(() => { if (playOnHover) lottieRef.current?.play(); }, [playOnHover]); const handleMouseLeave = useCallback(() => { if (playOnHover) lottieRef.current?.stop(); }, [playOnHover]); return ( <div style={{ width: size, height: size }} className={className} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} > <Lottie lottieRef={lottieRef} animationData={animationData} loop={loop} autoplay={!playOnHover} /> </div> ); }
Usage:
tsxcopyimport LottieIcon from "@/components/LottieIcon"; import arrowData from "@/public/icons/arrow.json"; // Looping animation <LottieIcon animationData={arrowData} /> // Plays only on hover <LottieIcon animationData={arrowData} playOnHover size={24} />