Skip to main content
Technology & EngineeringAnimation Services329 lines

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.

Quick Summary28 lines
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 lines
Paste into your CLAUDE.md or agent config

Lottie

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. 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.

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

Get CLI access →