Skip to content

Fix: Lottie Not Working — Animation Not Playing, File Not Loading, or React Component Blank

FixDevs ·

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 JSON

Or the animation plays once and stops:

Animation runs through once then disappears

Why 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.
  • loop defaults to true in lottie-react but false in lottie-web — depending on which library you use, the default loop behavior differs. An animation that plays once and stops has loop: 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles