Lottie in React and in a single HTML file

A Lottie animation is a JSON file, and lottie-web is the player that draws it in the browser. In React you wrap it in a component. In plain HTML you need one script tag and a div.

Lottie is an animation format: a JSON file that describes shapes, layers and keyframes. lottie-web is the JavaScript player that reads that JSON and draws it in the browser.

A lottie react example wraps that player in a React component, which needs npm and a bundler.

In a single HTML file you need much less: one script tag for lottie-web, an empty div, and one call to lottie.loadAnimation().

Here is the whole thing. The ball is a Lottie animation written straight into the page.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Lottie in one HTML file</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  /* The animation fills this box, so the box sets the size */
  #anim { width: 220px; height: 220px; margin: 0 auto; background: #fff; border-radius: 14px; }
  p { margin: 12px 0 0; text-align: center; font-size: 14px; color: #5b6270; }
</style>
</head>
<body>
<div id="anim"></div>
<p id="info">Loading…</p>

<!-- 1. Load lottie-web first (pinned version) -->
<script src="https://cdn.jsdelivr.net/npm/lottie-web@5.13.0/build/player/lottie.min.js"></script>

<!-- 2. The animation data, pasted in as a JavaScript object -->
<script>
const ease = { i: { x: 0.5, y: 1 }, o: { x: 0.5, y: 0 } };
const ballData = {
  v: '5.7.0', fr: 30, ip: 0, op: 60, w: 200, h: 200, nm: 'ball', ddd: 0, assets: [],
  layers: [{
    ddd: 0, ind: 1, ty: 4, nm: 'ball', sr: 1, ao: 0, ip: 0, op: 60, st: 0, bm: 0,
    ks: {
      o: { a: 0, k: 100 }, r: { a: 0, k: 0 }, a: { a: 0, k: [0, 0, 0] },
      s: { a: 0, k: [100, 100, 100] },
      // Position: up and down again over 60 frames
      p: { a: 1, k: [
        { t: 0,  s: [100, 150, 0], ...ease },
        { t: 30, s: [100, 50, 0],  ...ease },
        { t: 60, s: [100, 150, 0] }
      ] }
    },
    shapes: [{ ty: 'gr', it: [
      { ty: 'el', p: { a: 0, k: [0, 0] }, s: { a: 0, k: [50, 50] }, d: 1 },
      { ty: 'fl', c: { a: 0, k: [0.06, 0.46, 0.43, 1] }, o: { a: 0, k: 100 }, r: 1 },
      { ty: 'tr', p: { a: 0, k: [0, 0] }, a: { a: 0, k: [0, 0] }, s: { a: 0, k: [100, 100] },
        r: { a: 0, k: 0 }, o: { a: 0, k: 100 } }
    ] }]
  }]
};
</script>

<!-- 3. Start it -->
<script>
const anim = lottie.loadAnimation({
  container: document.getElementById('anim'),
  renderer: 'svg',
  loop: true,
  autoplay: true,
  animationData: ballData   // not path: works without a server
});

anim.addEventListener('DOMLoaded', () => {
  document.getElementById('info').textContent =
    'lottie-web ' + lottie.version + ' · ' + anim.totalFrames + ' frames at ' + anim.frameRate + ' fps';
});
</script>
</body>
</html>
A Lottie animation from one script tag and a div. Edit a keyframe and the example reruns.

The lottie-web repository is published under the MIT license. On 1 October 2026 the latest version on npm is 5.13.0, and that is the version every example here loads.

What a Lottie file is

The lottie-web README describes the usual path. An animator builds the animation in Adobe After Effects and exports it as JSON with the Bodymovin extension. lottie-web then renders that JSON on the page.

After Effects, Bodymovin, a JSON file, then lottie-web draws it in your div.
After Effects, Bodymovin, a JSON file, then lottie-web draws it in your div.

The Lottie Animation Community describes Lottie as an open-source vector animation file format based on JSON. Because it is vector data, the animation stays sharp at any size.

You do not need After Effects to follow this guide. The examples use small JSON objects written by hand, so you can see every part of them.

The smallest working file

Copy this into an empty .html file and open it in a browser. Replace the object after const data = with the contents of your own .json file.

<!doctype html>
<meta charset="utf-8">
<div id="anim" style="width: 300px; height: 300px"></div>

<script src="https://cdn.jsdelivr.net/npm/lottie-web@5.13.0/build/player/lottie.min.js"></script>
<script>
  const data = { /* paste the contents of your .json file here */ };

  lottie.loadAnimation({
    container: document.getElementById('anim'),
    renderer: 'svg',
    loop: true,
    autoplay: true,
    animationData: data
  });
</script>
  1. The div is where lottie-web draws. It needs a size, because the animation fills it.
  2. The first script tag loads lottie.min.js. It puts one global, lottie, on the page.
  3. The second script tag holds the animation data and calls loadAnimation(). The README lists 'svg', 'canvas' and 'html' as renderers.

The same pinned file is also on cdnjs and unpkg. All three addresses below load 5.13.0:

<script src="https://cdn.jsdelivr.net/npm/lottie-web@5.13.0/build/player/lottie.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lottie-web/5.13.0/lottie.min.js"></script>
<script src="https://unpkg.com/lottie-web@5.13.0/build/player/lottie.min.js"></script>

The lottie react example, and what it needs

In a React project, the lottie-react package gives you a component:

// npm i lottie-react
import { Lottie } from "lottie-react";
import animation from "./hero.json";

export function Hero() {
  return <Lottie src={animation} autoplay loop />;
}

The lottie-react README says src takes a path, a URL or the parsed animation object, and that the animation fills its element. React and react-dom 18.2 or newer are peer dependencies, and lottie-web is installed with it.

lottie-react needs a React project and a bundler. lottie-web needs one script tag.
lottie-react needs a React project and a bundler. lottie-web needs one script tag.

That file does not run on its own. React's docs say JSX is transformed into plain JavaScript under the hood, and in our test JSX in a plain <script> tag stopped with "Unexpected token '<'".

The import line stops with "Cannot use import statement outside a module". A React project runs this through a bundler first.

If you want a single page you can open and send, use lottie-web directly. The component above and the loadAnimation() call in the previous section draw the same animation with the same engine.

Hosting a React app as a page covers the other route, where you build first and share the output.

Why path: 'data.json' shows nothing

The README's own example loads the animation with path instead of animationData. lottie-web then downloads the JSON with a request. Open that page from your disk and the box stays empty.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Lottie: path vs animationData</title>
<style>
  * { box-sizing: border-box; }
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
  .col { background: #fff; border-radius: 12px; padding: 10px; border: 2px solid #e1e4ea; }
  .col.bad { border-color: #f3d1c8; } .col.good { border-color: #cfe9d7; }
  h2 { margin: 0 0 6px; font-size: 14px; }
  code { font-size: 12.5px; background: #eef1f5; padding: 1px 4px; border-radius: 4px; }
  .box { height: 150px; margin-top: 8px; border-radius: 8px; background: #f8f9fb; }
  .msg { margin: 8px 0 0; font-size: 13px; line-height: 1.4; min-height: 36px; }
  .bad .msg { color: #9a3412; } .good .msg { color: #0f5132; }
</style>
</head>
<body>
<div class="row">
  <div class="col bad">
    <h2>path</h2>
    <code>path: 'ball.json'</code>
    <div class="box" id="byPath"></div>
    <p class="msg" id="pathMsg">Waiting for ball.json…</p>
  </div>
  <div class="col good">
    <h2>animationData</h2>
    <code>animationData: ballData</code>
    <div class="box" id="byData"></div>
    <p class="msg" id="dataMsg">Loading…</p>
  </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/lottie-web@5.13.0/build/player/lottie.min.js"></script>
<script>
// Same bouncing ball as the first example
const ease = { i: { x: 0.5, y: 1 }, o: { x: 0.5, y: 0 } };
const ballData = {
  v: '5.7.0', fr: 30, ip: 0, op: 60, w: 200, h: 200, nm: 'ball', ddd: 0, assets: [],
  layers: [{
    ddd: 0, ind: 1, ty: 4, nm: 'ball', sr: 1, ao: 0, ip: 0, op: 60, st: 0, bm: 0,
    ks: {
      o: { a: 0, k: 100 }, r: { a: 0, k: 0 }, a: { a: 0, k: [0, 0, 0] },
      s: { a: 0, k: [100, 100, 100] },
      p: { a: 1, k: [
        { t: 0, s: [100, 150, 0], ...ease },
        { t: 30, s: [100, 50, 0], ...ease },
        { t: 60, s: [100, 150, 0] }
      ] }
    },
    shapes: [{ ty: 'gr', it: [
      { ty: 'el', p: { a: 0, k: [0, 0] }, s: { a: 0, k: [50, 50] }, d: 1 },
      { ty: 'fl', c: { a: 0, k: [0.06, 0.46, 0.43, 1] }, o: { a: 0, k: 100 }, r: 1 },
      { ty: 'tr', p: { a: 0, k: [0, 0] }, a: { a: 0, k: [0, 0] }, s: { a: 0, k: [100, 100] },
        r: { a: 0, k: 0 }, o: { a: 0, k: 100 } }
    ] }]
  }]
};

// Right: the data is already in the page, nothing to download
const right = lottie.loadAnimation({
  container: document.getElementById('byData'),
  renderer: 'svg', loop: true, autoplay: true,
  animationData: ballData
});
right.addEventListener('DOMLoaded', () => {
  document.getElementById('dataMsg').textContent = 'Playing. The JSON is inside the page.';
});

// Left: lottie has to download the JSON file
const failed = () => {
  document.getElementById('pathMsg').textContent =
    'Failed: the browser could not load ball.json. The box stays empty.';
};
try {
  // From a file the load fails with a data_failed event; on a page with no
  // address of its own (like this example frame) the request cannot even start.
  lottie.loadAnimation({
    container: document.getElementById('byPath'),
    renderer: 'svg', loop: true, autoplay: true,
    path: 'ball.json'
  }).addEventListener('data_failed', failed);
} catch (e) {
  failed();
}
</script>
</body>
</html>
Left: path asks the browser to download ball.json, and it fails. Right: the same data inside the page plays.

Chromium blocks requests from a page opened with a file:// address. The console shows "blocked by CORS policy", and lottie-web fires its data_failed event. This happened in our test even with ball.json sitting next to the HTML file.

path needs the JSON served over http. animationData has nothing to download.
path needs the JSON served over http. animationData has nothing to download.

There are two ways out:

  • Paste the data into the page. Open the .json file, copy everything, and put it after const data =. JSON is valid JavaScript object syntax, so nothing else changes.
  • Serve the files over http. A local server or a host makes path work. The README notes that path and animationData are mutually exclusive.

For one self-contained file, the first way is simpler. It is also the only one that keeps working once the page is shared as a single file.

Setting the size

lottie-web adds an <svg> to your div with width: 100% and height: 100%. The animation keeps its own shape and fills the div.

In our test, a div with no size set got an animation as wide as the page, with the height following the animation's own ratio. Give the div a width and height, or a width and aspect-ratio, and the animation follows.

#anim { width: 240px; aspect-ratio: 1; }

The rendererSettings option takes preserveAspectRatio, with the same values as the SVG attribute, if you need the animation to crop instead of fit.

Controlling playback

loadAnimation() returns an animation object. Keep it in a variable and you can play, stop and jump with it. This example plays a check mark when a form is submitted.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Lottie success check on a form</title>
<style>
  * { box-sizing: border-box; }
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .card { max-width: 420px; margin: 0 auto; padding: 18px; border-radius: 14px; background: #fff; }
  h2 { margin: 0 0 10px; font-size: 19px; }
  form { display: flex; gap: 8px; flex-wrap: wrap; }
  input[type=email] { flex: 1 1 180px; min-width: 0; font: inherit; padding: 9px 10px; border: 1px solid #c9ced8; border-radius: 8px; }
  button { font: inherit; padding: 9px 14px; border: 0; border-radius: 8px; background: #1d2330; color: #fff; cursor: pointer; }
  #check { width: 120px; height: 120px; margin: 12px auto 0; }
  #out { margin: 4px 0 0; min-height: 22px; text-align: center; font-size: 14px; color: #0f5132; }
  .tools { margin-top: 12px; padding-top: 12px; border-top: 1px solid #eceef2; font-size: 13px; color: #5b6270; }
  .tools label { display: flex; align-items: center; gap: 8px; margin-top: 6px; }
  .tools input[type=range] { flex: 1; }
</style>
</head>
<body>
<div class="card">
  <h2>Join the list</h2>
  <form id="signup">
    <input type="email" name="email" placeholder="you@example.com" required>
    <button>Sign up</button>
  </form>
  <div id="check"></div>
  <p id="out"></p>

  <div class="tools">
    Inspect the animation:
    <label>Frame <input type="range" id="scrub" min="0" max="39" value="0"> <span id="frame">0</span></label>
    <label>Speed
      <select id="speed"><option value="0.5">0.5×</option><option value="1" selected>1×</option><option value="2">2×</option></select>
    </label>
  </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/lottie-web@5.13.0/build/player/lottie.min.js"></script>
<script>
const v = (k) => ({ a: 0, k });                         // a fixed value
const anim1 = (from, to, t0, t1) => ({ a: 1, k: [       // an animated value
  { t: t0, s: from, i: { x: 0.3, y: 1 }, o: { x: 0.6, y: 0 } },
  { t: t1, s: to }
] });
const tr = { ty: 'tr', p: v([0, 0]), a: v([0, 0]), s: v([100, 100]), r: v(0), o: v(100) };

const checkData = {
  v: '5.7.0', fr: 30, ip: 0, op: 40, w: 120, h: 120, nm: 'check', ddd: 0, assets: [],
  layers: [
    { // the tick: a path drawn on with Trim Paths
      ddd: 0, ind: 1, ty: 4, nm: 'tick', sr: 1, ao: 0, ip: 0, op: 40, st: 0, bm: 0,
      ks: { o: v(100), r: v(0), a: v([0, 0, 0]), s: v([100, 100, 100]), p: v([60, 60, 0]) },
      shapes: [{ ty: 'gr', it: [
        { ty: 'sh', ks: v({ c: false, v: [[-24, 2], [-7, 19], [25, -15]],
          i: [[0, 0], [0, 0], [0, 0]], o: [[0, 0], [0, 0], [0, 0]] }) },
        { ty: 'tm', s: v(0), e: anim1([0], [100], 14, 32), o: v(0), m: 1 },
        { ty: 'st', c: v([1, 1, 1, 1]), o: v(100), w: v(9), lc: 2, lj: 2 },
        tr
      ] }]
    },
    { // the green circle: grows from 0 to full size
      ddd: 0, ind: 2, ty: 4, nm: 'circle', sr: 1, ao: 0, ip: 0, op: 40, st: 0, bm: 0,
      ks: { o: v(100), r: v(0), a: v([0, 0, 0]), p: v([60, 60, 0]),
        s: anim1([0, 0, 100], [100, 100, 100], 0, 16) },
      shapes: [{ ty: 'gr', it: [
        { ty: 'el', p: v([0, 0]), s: v([104, 104]), d: 1 },
        { ty: 'fl', c: v([0.09, 0.64, 0.29, 1]), o: v(100), r: 1 },
        tr
      ] }]
    }
  ]
};

const check = lottie.loadAnimation({
  container: document.getElementById('check'),
  renderer: 'svg',
  loop: false,
  autoplay: false,          // wait for the form
  animationData: checkData
});

const scrub = document.getElementById('scrub');
const frame = document.getElementById('frame');

// Keep the slider in step while it plays
check.addEventListener('enterFrame', () => {
  const f = Math.round(check.currentFrame);
  scrub.value = f;
  frame.textContent = f;
});

document.getElementById('signup').addEventListener('submit', (e) => {
  e.preventDefault();                     // nothing is sent anywhere
  const email = new FormData(e.target).get('email');
  check.goToAndPlay(0, true);             // frame 0, then play
  document.getElementById('out').textContent = 'Signed up: ' + email;
});

scrub.addEventListener('input', () => {
  check.goToAndStop(Number(scrub.value), true);   // true = value is a frame
  frame.textContent = scrub.value;
});

document.getElementById('speed').addEventListener('change', (e) => {
  check.setSpeed(Number(e.target.value));
});
</script>
</body>
</html>
Submit the form and the check plays from frame 0. Drag the slider to step through the frames.

The form handler calls preventDefault(), so nothing is sent anywhere. It reads the email with FormData and shows it on the page.

Method What it does
play() / pause() Start or pause where it is
stop() Stop and go back to the start
setSpeed(2) 1 is normal speed
setDirection(-1) -1 plays in reverse
goToAndStop(10, true) Jump to frame 10 and stay
goToAndPlay(0, true) Jump to frame 0 and play
playSegments([0, 20], true) Play only frames 0 to 20
destroy() Remove the animation

The true in goToAndStop matters. The README says the second argument decides whether the first number is a frame or a time, and it defaults to time.

The object also fires events. Listen with addEventListener for complete, loopComplete, enterFrame, DOMLoaded and data_failed. The example uses enterFrame to move the slider while the check plays.

When it does not work

What you see Cause Fix
lottie is not defined Your code runs above the lottie-web tag Put your script after it
Empty box, CORS error in the console path on a page opened from file:// Use animationData
Empty box, no error Your script runs before the div exists Put the script below the div
The animation fills the whole width The div has no size Set a width and height on the div
It plays on load, not on click autoplay: true Set autoplay: false, call play()
goToAndStop(10) lands in the wrong place The number is read as time Pass true as the second argument
"Cannot use import statement outside a module" A React example pasted into plain HTML Use the script tag and loadAnimation()

For console errors in general, HTML JavaScript not working shows where to look.

An animation is hard to show in a screenshot, and a still image of a check mark says little.

An .html attachment may open as plain code on a phone, and a path to a separate JSON file breaks as soon as the two files are apart.

With the data inside the page, the whole animation is one file. Paste it into a NOS document and choose Create share link. HTML to link walks through it.

The page renders with its scripts running, lottie-web loads from the CDN, and anyone with the link can play it without an account. If you change a keyframe later, the same link shows the new version.

Questions people ask

Do I need React to use Lottie on a web page?

No. lottie-web is a plain JavaScript library. Load lottie.min.js with a script tag and call lottie.loadAnimation() with a container element and the animation data. lottie-react is a React wrapper, and npm installs lottie-web along with it.

Why does my Lottie animation not show when I open the HTML file?

If you used path: 'data.json', lottie-web downloads the JSON with a request, and Chromium blocks requests from a page opened from file://. The animation fires data_failed and the box stays empty. Paste the JSON into the page and pass it as animationData.

Is lottie-web free?

Yes. The lottie-web repository and its npm package are published under the MIT license.

Which lottie-web version should I use?

On 1 October 2026 the latest version on npm is 5.13.0. Put the version in the CDN address, such as lottie-web@5.13.0, so the page does not change when a new release comes out.

Can I paste a lottie-react example into an HTML file?

Not as it is. The import line and the JSX need a bundler. In a plain script tag, the import line stops with "Cannot use import statement outside a module". Use lottie.loadAnimation() instead, which is what the React component calls for you.

Keep reading