EmpireUI
Get Pro
← Blog8 min read#lottie#react#animation

Lottie Animations in React: Setup, Optimisation and Pitfalls

Learn how to integrate Lottie JSON animations into React apps — from first install to lazy loading, loop control, and avoiding the bundle-size traps most devs hit.

Colorful abstract motion graphics representing JSON-driven animation in a React app

What Lottie Actually Is (and Why You'd Reach for It)

Lottie is a JSON-based animation format exported directly from After Effects via the Bodymovin plugin. Instead of shipping a 2 MB GIF or a video file, you ship a lightweight .json file that a renderer — lottie-web, @lottiefiles/react-lottie-player, or lottie-react — plays back in the browser at any resolution without pixelation.

Honestly, the format took off around 2017–2018 because mobile teams at Airbnb needed designer-quality animation without the handoff nightmare of CSS keyframes. The browser renderer is vector-based, so a 48px icon and a 480px hero animation use the exact same file.

That said, Lottie isn't always the right call. If your animation is simple — a basic fade or a 200ms slide — CSS or Framer Motion will outperform it on first paint and give you fewer dependencies to manage. Lottie pays off when the animation is genuinely complex: multipart illustrations, morphing paths, or anything a designer built frame-by-frame in AE.

Worth noting: the JSON files themselves can be surprisingly large. A 5-second animated illustration might clock in at 80–150 KB unparsed. That's fine for a hero section loaded once, but careless if you're throwing Lottie into every card on a feed page.

Installing and Wiring Up lottie-react in 2026

The cleanest React integration right now is lottie-react (v2.4+). It wraps lottie-web in a hook-friendly API with TypeScript types out of the box. Install it with your package manager of choice:

npm install lottie-react
# or
pnpm add lottie-react

Then the most basic usage looks like this:

import Lottie from 'lottie-react';
import confettiData from './confetti.json';

export function ConfettiBlast() {
  return (
    <Lottie
      animationData={confettiData}
      loop={false}
      style={{ width: 320, height: 320 }}
    />
  );
}

The animationData prop accepts a plain JS object, so you can import the JSON directly — Webpack, Vite, and Next.js all handle that without any extra config. One more thing — always specify width and height explicitly (even if it's just via a CSS class). Without it, Lottie defaults to 100% width and the canvas can jump around during hydration in SSR setups.

In practice, you'll also want the lottieRef to control playback imperatively. The package exposes a useLottie hook that returns the player instance, so you can call .play(), .pause(), or .goToAndStop(frame, true) directly in event handlers.

Controlling Playback: Loops, Segments and Triggers

The default loop={true} auto-plays forever — fine for loading spinners, annoying everywhere else. For anything triggered by user interaction, you want manual control.

import Lottie from 'lottie-react';
import { useRef } from 'react';
import checkmarkData from './checkmark.json';

export function CheckmarkButton({ onConfirm }) {
  const lottieRef = useRef();

  const handleClick = () => {
    lottieRef.current?.play();
    onConfirm?.();
  };

  return (
    <button onClick={handleClick} className="icon-btn">
      <Lottie
        lottieRef={lottieRef}
        animationData={checkmarkData}
        loop={false}
        autoplay={false}
        style={{ width: 40, height: 40 }}
      />
      Confirm
    </button>
  );
}

Segments are underused. If you've got a multi-state animation — idle → loading → success — all baked into a single JSON file, you can jump between frame ranges with playSegments([[0, 30], [31, 60]], true). That keeps your JSON count down and lets a designer build one cohesive file instead of three separate exports.

Quick aside: the onComplete callback fires after a non-looping animation finishes. Use it to swap out the Lottie for a static image or trigger the next UI state. Keeping the canvas mounted after completion burns unnecessary GPU for zero visual gain.

Performance Optimisation You Actually Need

The biggest bundle-size trap: importing the animation JSON at the top of your component file means it's bundled into your main chunk. For large files — anything above 30 KB — lazy load them instead.

import { useState, useEffect } from 'react';
import Lottie from 'lottie-react';

export function LazyLottie({ src, ...props }) {
  const [animData, setAnimData] = useState(null);

  useEffect(() => {
    fetch(src)
      .then(r => r.json())
      .then(setAnimData);
  }, [src]);

  if (!animData) return <div className="lottie-placeholder" />;
  return <Lottie animationData={animData} {...props} />;
}

// Usage: <LazyLottie src="/animations/hero.json" loop autoplay />

Serve the JSON from your public folder (or a CDN) and it'll load independently of your JS bundle. Next.js's next/dynamic with ssr: false is another solid path if you're working in App Router and want to avoid hydration mismatches entirely.

On the renderer side, lottie-web defaults to the SVG renderer. It's the most compatible, but if you're animating on a canvas-heavy page or running many instances simultaneously, switch to renderer='canvas' — it's faster to paint, just lower quality for complex gradients. The renderer='html' option exists but it's missing a lot of AE features and you'll hit broken animations fast.

Honestly, the single most impactful thing you can do is preoptimise your JSON with LottieFiles' optimiser or the lottie-minify CLI. On a real project in 2024 we took a 120 KB file to 47 KB without any visible difference. That's a 60% reduction for free.

Common Pitfalls and How to Dodge Them

SSR crashes are the most common issue when you first pull Lottie into a Next.js project. The library references document and window directly, so any server-side render will throw. Wrap your component in dynamic(() => import('./YourLottieComponent'), { ssr: false }) and it disappears.

The second trap is forgetting to call lottieRef.current?.destroy() on unmount when you're managing the player manually. The renderer attaches event listeners and a rAF loop — skip cleanup and you're leaking memory, especially on pages with route transitions.

Look, the third issue is one that bites designers and devs equally: fonts in Lottie. After Effects text layers don't export into the JSON — the renderer has no font system. If your animation has text, the designer needs to convert it to outlines before exporting, or you'll see blank spaces where the copy should be.

One more thing — responsive sizing. Lottie respects the container dimensions at instantiation but doesn't automatically reflow on window resize unless you call resize() on the player instance. Add a ResizeObserver on the container div if you need the animation to scale dynamically, like in a fluid grid layout. If you're building those grids, browse the components at Empire UI for layout primitives that pair well with animated elements.

Accessibility Considerations

Animated content is a real concern for users with vestibular disorders. The prefers-reduced-motion media query should always gate your Lottie playback. The pattern is simple:

import { useEffect, useRef } from 'react';
import Lottie from 'lottie-react';
import heroData from './hero.json';

export function AccessibleHeroAnimation() {
  const lottieRef = useRef();
  const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  useEffect(() => {
    if (prefersReduced) {
      lottieRef.current?.goToAndStop(0, true);
    }
  }, [prefersReduced]);

  return (
    <div role="img" aria-label="Animated illustration of a dashboard">
      <Lottie
        lottieRef={lottieRef}
        animationData={heroData}
        loop={!prefersReduced}
        autoplay={!prefersReduced}
      />
    </div>
  );
}

Wrap the Lottie in a div with role="img" and a meaningful aria-label. Screen readers can't interpret canvas or SVG animation content — without the label, users on assistive tech get nothing. That's not a nice-to-have; it's table stakes for any production UI.

If the animation is purely decorative — a background particle effect, say — use aria-hidden="true" on the wrapper and skip the label entirely. The distinction matters: decorative vs. informative should drive your ARIA approach, same as it does for static images. For more UI pattern examples, the glassmorphism components page shows how Empire UI handles decorative layering without accessibility debt.

Picking the Right Library for Your Setup

There are three main options in 2026. lottie-react is the default choice — good TypeScript support, hook API, minimal overhead. @lottiefiles/react-lottie-player adds a built-in player UI with controls, useful for demo pages or admin tools. @dotlottie/react supports the newer .lottie container format (a zipped JSON with assets), which can cut file sizes another 30–40% at the cost of slightly less ecosystem tooling.

The .lottie format is worth considering if your team is generating animations programmatically or managing a large library of files. It bundles assets (like bitmap textures) inside a single archive, so you're not juggling a JSON file plus a separate /images folder. That said, not every After Effects plugin exports .lottie natively yet — check your designer's toolchain before committing.

For teams building heavily animated interfaces — think cyberpunk or vaporwave aesthetic UIs — Lottie pairs well with CSS custom properties driving color overrides. The lottie-web API has a setSubframeRendering method and colour replacement utilities that let you theme the same JSON file to match a dark/light toggle without re-exporting from AE. That's a significant workflow win.

FAQ

Can I use Lottie in a Next.js App Router component?

Yes, but mark it as a Client Component with 'use client' at the top, or wrap it in next/dynamic with ssr: false. Lottie touches the DOM directly and will crash on the server otherwise.

Why is my Lottie animation blurry on retina screens?

You're probably using the canvas renderer with a low devicePixelRatio. Switch to the SVG renderer (renderer='svg') — it's resolution-independent and renders crisply at any pixel density.

How do I change Lottie animation colours at runtime?

Use lottie-colorify or call lottieRef.current?.renderer.elements to walk the tree and update fill colours. The cleaner approach is designing the JSON with named colour slots and using setColor from lottie-web's utils.

What's the difference between lottie-react and @lottiefiles/react-lottie-player?

lottie-react is a thin hook-based wrapper — minimal API, good for custom control. The LottieFiles player adds a built-in progress bar and controls UI, which is handy for prototypes but overkill for most production components.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

HTML Canvas Animations in React: Particles, Noise Fields, MoreAudio Visualizer in React: Web Audio API + Canvas = Waveform MagicLottie Animations in React 2026: @lottiefiles/react, DotLottie SetupPage Load Animation in React: First Paint, Stagger, Skeleton to Content