Lottie Animations in Next.js with lottie-react: Setup to Playback Control

Published: October 8, 2026 · 8 min read

Use Lottie in Next.js with lottie-react: loading JSON via import or /public, dynamic import for a lighter first load, playing only in view, and reduced motion, with code.

Your designer sent a Lottie animation. Here is how to run it in Next.js

Your designer handed over the hero animation as a JSON file, and now you are stuck on how to get it into your Next.js app.

That JSON is a Lottie animation. In React, you display it with a library called lottie-react.

Here is the short answer: put the JSON in the public folder and pass its path to lottie-react's Lottie component. Then load it after the page and play it only while it is on screen, and the page stays light.

This guide covers:

  • Installing lottie-react and the shortest code that shows an animation

  • Importing the JSON versus loading it from public

  • Dynamic import to cut the JavaScript loaded up front

  • Playing when the animation enters the screen and pausing when it leaves

  • Reduced motion support and screen reader considerations

The code assumes the Next.js 15 App Router and lottie-react 3.

Why use Lottie animations

Lottie is an animation format: motion built in tools like After Effects, exported as JSON. From After Effects, the usual exporter is a plugin called Bodymovin.

Compared with GIFs and video, it has three main advantages:

  • Shapes are stored as numbers, so lines stay crisp at any size

  • Play, pause, and jumping to a specific frame can be controlled from code

  • For line- and shape-based motion, the file tends to be smaller than an equivalent video

It is a poor fit for photos or live footage, where video is lighter. Building a hero on a background video is covered in https://design-layer.com/en/articles/nextjs-video-hero-guide.

Sites such as LottieFiles host public animations you can browse. Check each license before you use one.

Three steps to show Lottie in Next.js

From least to most effort:

  • Step 1: Install lottie-react and display the JSON

  • Step 2: Use dynamic import so it loads after the page renders

  • Step 3: Switch between playing and paused as it enters and leaves the screen

Step 1 is enough for a small animated icon. For a large animation in the hero, go all the way to Step 3.

Step 1: Display a Lottie animation with lottie-react

Install the library with npm i lottie-react. lottie-web, the engine that does the drawing, comes with it.

Put the JSON you received at public/lottie/plant.json. Files in public are served at a URL like /lottie/plant.json.

Keep the display component in its own file marked with "use client", the marker for browser-side code. When you add playback control later, this is the only file you touch.

components/LottiePlayer.tsx
"use client";

import { Lottie } from "lottie-react";

type Props = {
  src: string | object;
  className?: string;
};

export default function LottiePlayer({ src, className }: Props) {
  return (
    <Lottie
      src={src}
      autoplay
      loop
      aria-hidden="true"
      className={className}
    />
  );
}

Size the Lottie component with className

The Lottie component fills whatever box it sits in. A box with no height shows nothing.

Set the size with className, such as aspect-square or h-64. A fixed aspect ratio also keeps the layout from shifting when the animation appears.

Importing the JSON versus loading it from public

You can also import the JSON as a module. The animation data then ships inside your JavaScript.

  • import: no wait, it draws right away. In exchange, your JavaScript grows by the size of the JSON

  • public path: your JavaScript stays light. The JSON is fetched separately just before it is shown

As a rule of thumb, import small icons of a few KB, and serve hero animations of tens of KB or more from public.

components/PlantIcon.tsx
"use client";

import LottiePlayer from "./LottiePlayer";
import plant from "@/animations/plant.json";

export function PlantIcon() {
  return <LottiePlayer src={plant} className="size-12" />;
}

Step 2: Dynamically import Lottie to lighten the first load

lottie-react's engine (the full build of lottie-web 5.13) is about 300KB before compression. Loaded normally, it adds that much to the page's initial JavaScript.

With Next.js dynamic, you split this component into its own chunk and load it later. Adding ssr: false skips server rendering and loads it only in the browser.

Older version combinations could throw "document is not defined" during server rendering. Loading only in the browser removes that worry too.

components/LazyLottie.tsx
"use client";

import dynamic from "next/dynamic";

const LottiePlayer = dynamic(() => import("./LottiePlayer"), {
  ssr: false,
});

type Props = {
  src: string;
};

export function LazyLottie({ src }: Props) {
  return (
    <div className="mx-auto aspect-square w-full max-w-sm">
      <LottiePlayer src={src} className="h-full w-full" />
    </div>
  );
}

Write ssr: false inside a browser-side file

In Next.js 15, ssr: false is not allowed inside a Server Component; the build fails.

That is why dynamic is called inside LazyLottie, which carries the marker. The page just renders this component as usual.

app/page.tsx
import { LazyLottie } from "@/components/LazyLottie";

export default function Page() {
  return (
    <section className="mx-auto grid max-w-5xl items-center gap-10 px-6 py-24 md:grid-cols-2">
      <div>
        <h1 className="text-4xl font-semibold tracking-tight">
          Team knowledge that grows like a plant
        </h1>
        <p className="mt-4 text-zinc-600">
          Notes link up on their own, so you find them when you need them.
        </p>
      </div>
      <LazyLottie src="/lottie/plant.json" />
    </section>
  );
}

Reserve the box before Lottie loads

With dynamic import, nothing is drawn until the component arrives. The outer div has an aspect ratio so the space is reserved first.

Without it, everything below gets pushed down the moment the animation appears. That shift hurts CLS, the layout stability metric.

Step 3: Play Lottie only while it is on screen

An animation that keeps running off screen spends processing on motion nobody sees, and on phones it drains the battery.

lottie-react 3 ships lottieInView, which plays when the animation enters the screen and pauses when it leaves. Wrap it in LottieInteractions and set autoplay to false.

Rewrite the Step 1 LottiePlayer as below. Add label and stillFrame to LazyLottie's props too, and pass them through.

components/LottiePlayer.tsx
"use client";

import { useRef } from "react";
import {
  Lottie,
  LottieInteractions,
  lottieInView,
  type LottieHandle,
} from "lottie-react";
import { useReducedMotion } from "motion/react";

type Props = {
  src: string | object;
  className?: string;
  /** A description if the animation carries meaning. Omit it for decoration */
  label?: string;
  /** The frame to hold when the user prefers reduced motion */
  stillFrame?: number;
};

export default function LottiePlayer({ src, className, label, stillFrame = 0 }: Props) {
  const lottieRef = useRef<LottieHandle>(null);
  const reduceMotion = useReducedMotion();

  return (
    <LottieInteractions interactions={reduceMotion ? [] : [lottieInView({ amount: 0.3 })]}>
      <Lottie
        src={src}
        lottieRef={lottieRef}
        autoplay={false}
        loop
        className={className}
        role={label ? "img" : undefined}
        aria-label={label}
        aria-hidden={label ? undefined : true}
        subscriptions={{
          ready: () => {
            if (reduceMotion) lottieRef.current?.seek(stillFrame);
          },
        }}
      />
    </LottieInteractions>
  );
}

Key points in the code

  • lottieInView pauses when the animation leaves the screen, and resumes mid-motion when it comes back

  • amount: 0.3 counts the animation as in view once 30% of it is visible

  • For reduced motion users it never plays, and holds the frame set by stillFrame instead

  • It is exposed to screen readers only when you pass label; otherwise aria-hidden hides it

While the tab is in the background, the browser itself stops drawing updates, so you normally do not need extra handling for tab switches.

Add a play/pause button with lottieRef

Animation that starts on its own and runs longer than five seconds needs a way to stop it. That is what WCAG success criterion 2.2.2 asks for.

lottieRef holds commands such as play and pause. Wire them to a button and readers can stop the motion themselves. The button is 44px square so it is easy to tap on phones.

components/LottieWithToggle.tsx
"use client";

import { useRef, useState } from "react";
import { Lottie, type LottieHandle } from "lottie-react";

export function LottieWithToggle({ src }: { src: string }) {
  const lottieRef = useRef<LottieHandle>(null);
  const [playing, setPlaying] = useState(false);

  return (
    <div className="relative mx-auto aspect-square w-full max-w-sm">
      <Lottie
        src={src}
        lottieRef={lottieRef}
        autoplay
        loop
        aria-hidden="true"
        className="h-full w-full"
        subscriptions={{
          play: () => setPlaying(true),
          pause: () => setPlaying(false),
          stop: () => setPlaying(false),
        }}
      />
      <button
        type="button"
        aria-label={playing ? "Pause animation" : "Play animation"}
        onClick={() => (playing ? lottieRef.current?.pause() : lottieRef.current?.play())}
        className="absolute bottom-3 right-3 grid size-11 place-items-center rounded-full bg-white/90 text-zinc-900 shadow focus-visible:outline focus-visible:outline-2 focus-visible:outline-zinc-900"
      >
        <span aria-hidden="true">{playing ? "❚❚" : "▶"}</span>
      </button>
    </div>
  );
}

Lottie animations, reduced motion, and accessibility

People who turn on settings like Reduce Motion in their OS can feel sick from on-screen movement. Browsers expose that setting as prefers-reduced-motion.

Under that setting, lottie-react 3 does not start autoplay; the animation sits on its first frame.

Some animations start from a blank first frame, though. In that case use Step 3's stillFrame to pick a frame close to the finished image.

Hide decorative animation from screen readers

A background plant or motion that only decorates a headline carries no meaning of its own. Hide it from screen readers with aria-hidden.

Put what you want to say in the headline and body text, not in the animation.

Describe animation that carries meaning

Sometimes motion conveys information, like a checkmark confirming a form was sent. Then give it role="img" and an aria-label that puts it into words.

For state changes like completion, also show the message as on-screen text to be safe.

Lottie animation pre-launch checks and size tips

Depending on how the JSON was made, files range from a few KB to several MB. Before shipping, check both the size and the motion.

Make the Lottie JSON file lighter

  • Do not embed photos. Images stored as strings inside the JSON make it heavy fast

  • Delete unused layers and invisible shapes before exporting

  • Export only the length you actually use

  • Confirm your server applies compression such as gzip

If the animation uses no After Effects expressions, you can switch to the lighter LottieLight. Its engine is about 170KB, a little over half of the full build.

Files ending in .lottie are zip archives, and lottie-react 3 cannot read them directly. Ask for a JSON export instead.

Common mistakes and fixes

  • The box has no height, so nothing shows. Set a size with className

  • The browser marker is on the whole page file. Move only the Lottie component into its own file

  • It keeps looping off screen. Stop it with lottieInView from Step 3

  • Headlines or images stay hidden until the animation ends. Keep text visible from the start

  • Several large animations on one page. Pick one lead

The fourth one delays LCP, the loading-speed metric. Adding motion to a hero is covered in https://design-layer.com/en/articles/nextjs-animated-hero-section-guide.

Copy a finished hero with a Lottie animation

The Green Scroll Hero page on DesignLayer: check the live preview on the left, then copy the source with Copy for Cursor on the right
The Green Scroll Hero page on DesignLayer: check the live preview on the left, then copy the source with Copy for Cursor on the right

A headline that emerges from behind a plant animation as you scroll needs playback control and scroll math working together. Building it yourself takes a lot of fine-tuning.

Another route is to try a finished section in the browser first, then use it. The Green Scroll Hero in the DesignLayer catalog reveals its headline while pinned during scroll, and its central foliage runs on lottie-react.

On the detail page, Copy for Cursor copies the integration instructions, the live preview URL, the component TSX, and the npm dependencies in one go. Paste them into Cursor or Claude to bring it into your Next.js app.

Browsing and live previews are free, with no account or card. Free source copies are 3 per day (JST). https://design-layer.com/en/components

After that, swap the headline, logo, and animation JSON. It suits the top of environmental, tech, and brand landing pages.

Lottie animations in Next.js: common questions

  • Install lottie-react from npm. Put the JSON in public and pass its path to the Lottie component's src.

    The file that renders it needs "use client", the browser-side marker.

Summary: how to use Lottie animations in Next.js

  • Lottie is an animation format: motion from tools like After Effects, saved as JSON

  • Install lottie-react, put the JSON in public, and pass its path to src

  • Defer loading with dynamic and ssr: false, and reserve the box size up front

  • Use lottieInView to play only while it is on screen

  • Hold a still frame under reduced motion, and mark decoration aria-hidden

Start by dropping one of your JSON files into the Step 1 code.

Scroll-driven motion in general is covered in https://design-layer.com/en/articles/nextjs-scroll-reveal-guide, and opening animations when a page loads in https://design-layer.com/en/articles/nextjs-loading-screen-guide.

Lottie Animations in Next.js with lottie-react: Setup to Playback Control | DesignLayer