react-lottie 2.3.1 → 2.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ## [2.3.3](https://github.com/chenqingspring/react-lottie/compare/v2.3.2...v2.3.3) (2026-11-08)
6
+
7
+ ### Documentation
8
+
9
+ * document animations that reference raster images: the `assets` array, the `assetsPath` option and rewriting assets to bundler-hashed URLs ([#49](https://github.com/chenqingspring/react-lottie/issues/49), [#59](https://github.com/chenqingspring/react-lottie/issues/59))
10
+ * document aliasing `lottie-web` to `lottie_light`, `lottie_light_canvas` or `lottie_light_html` to cut the bundled player by about 45% ([#79](https://github.com/chenqingspring/react-lottie/issues/79))
11
+ * document server-side rendering and add a regression test that renders the component in a Node process without a DOM ([#17](https://github.com/chenqingspring/react-lottie/issues/17))
12
+
13
+ ## [2.3.2](https://github.com/chenqingspring/react-lottie/compare/v2.3.1...v2.3.2) (2026-11-07)
14
+
15
+ ### Bug Fixes
16
+
17
+ * accept a namespace import (`import * as animationData`) by unwrapping its `default` export and clone frozen animation objects before handing them to lottie-web, so they no longer fail with `Cannot read property 'length' of undefined` (#24, #65)
18
+ * throw an actionable error when `options.animationData` is a string or when neither `animationData` nor `path` is provided, instead of lottie-web's `Cannot create property 'tf' on string` / `Cannot read property 'length' of undefined` deep in the renderer (#44)
19
+ * fix the README examples that read `.default` from a default import, which passed `undefined` as the animation data
20
+
5
21
  ## [2.3.1](https://github.com/chenqingspring/react-lottie/compare/v2.3.0...v2.3.1) (2026-11-05)
6
22
 
7
23
  ### Bug Fixes
package/README.md CHANGED
@@ -37,10 +37,76 @@ npm install --save react-lottie
37
37
  > [GitHub](https://github.com/chenqingspring/react-lottie/issues) and Relay
38
38
  > will review and fix it.
39
39
 
40
+ ## Using from a CDN (no bundler)
41
+
42
+ The published `dist/index.js` is a CommonJS build for bundlers and npm
43
+ consumers. It uses `exports` and expects `react`, `prop-types` and `lottie-web`
44
+ to be resolvable, so loading that file with a plain `<script>` tag fails with
45
+ `Uncaught ReferenceError: exports is not defined`. The package does not ship a
46
+ UMD/browser global build.
47
+
48
+ To use react-lottie without a build step, load it as an ES module from a CDN
49
+ and map `react` to the same copy the page uses. [esm.sh](https://esm.sh)
50
+ supports `?external=react` for exactly this:
51
+
52
+ ```html
53
+ <script type="importmap">
54
+ {
55
+ "imports": {
56
+ "react": "https://esm.sh/react@18",
57
+ "react-dom/client": "https://esm.sh/react-dom@18/client",
58
+ "react-lottie": "https://esm.sh/react-lottie@2?external=react"
59
+ }
60
+ }
61
+ </script>
62
+ <script type="module">
63
+ import React from 'react';
64
+ import { createRoot } from 'react-dom/client';
65
+ import Lottie from 'react-lottie';
66
+
67
+ const options = {
68
+ path: '/animations/pinjump.json',
69
+ loop: true,
70
+ autoplay: true,
71
+ };
72
+
73
+ createRoot(document.getElementById('root')).render(
74
+ React.createElement(Lottie, { options, height: 400, width: 400 })
75
+ );
76
+ </script>
77
+ ```
78
+
79
+ Without a compiler for JSX, create elements with `React.createElement`, or add
80
+ `htm`/Babel. Keeping `react` in the import map gives the page one React
81
+ instance; a CDN build that bundles its own React (such as jsDelivr's `+esm`
82
+ output) produces two copies, which breaks context and hooks.
83
+
84
+ If you only need to play an animation and not the React wrapper, load
85
+ lottie-web directly and skip React entirely:
86
+
87
+ ```html
88
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/bodymovin/5.12.2/lottie.min.js"></script>
89
+ <script>
90
+ lottie.loadAnimation({
91
+ container: document.getElementById('root'),
92
+ path: '/animations/pinjump.json',
93
+ renderer: 'svg',
94
+ loop: true,
95
+ autoplay: true,
96
+ });
97
+ </script>
98
+ ```
99
+
100
+ An ESM CDN build still downloads React, React DOM, lottie-web and react-lottie,
101
+ so it removes the build step but not the bytes. For production, a bundler that
102
+ shares one React copy and tree-shakes lottie-web is smaller.
103
+
40
104
  ## Requirements
41
105
 
42
- React 16.8 or newer. The test suite runs against React 18; the component uses
43
- plain class components and works with React 19 as well.
106
+ React 16.8 or newer, including React 17, React 18 and React 19. The
107
+ `react: ">=16.8.0"` peer dependency resolves without `--legacy-peer-deps` or
108
+ `--force`. The test suite runs against React 18; the component uses plain class
109
+ components and works with React 17 and React 19 as well.
44
110
 
45
111
  ## Local development
46
112
 
@@ -95,7 +161,7 @@ export default class LottieControl extends React.Component {
95
161
  const defaultOptions = {
96
162
  loop: true,
97
163
  autoplay: true,
98
- animationData: animationData.default,
164
+ animationData,
99
165
  rendererSettings: {
100
166
  preserveAspectRatio: 'xMidYMid slice'
101
167
  }
@@ -116,6 +182,11 @@ export default class LottieControl extends React.Component {
116
182
 
117
183
  ```
118
184
 
185
+ Pass the parsed animation object as `options.animationData`. A file path or URL
186
+ belongs in `options.path`; a namespace import
187
+ (`import * as animationData`) is unwrapped automatically but the default import
188
+ above is preferred.
189
+
119
190
  ### Loading animation data without bloating the bundle
120
191
 
121
192
  A static import like `import animationData from './pinjump.json'` (the example
@@ -184,6 +255,170 @@ Finally, the file itself can usually get smaller: export at a lower precision,
184
255
  remove unused layers and precompositions, avoid embedded raster images, and let
185
256
  gzip/Brotli compress the JSON. LottieFiles' optimizer automates the first steps.
186
257
 
258
+ ### Animations with raster images
259
+
260
+ An After Effects export that contains bitmap layers references those files from
261
+ the animation's `assets` array. Each entry carries a directory (`u`), a file name
262
+ (`p`) and an embedded flag (`e`):
263
+
264
+ ```json
265
+ { "id": "image_0", "w": 720, "h": 720, "u": "images/", "p": "img_0.png", "e": 0 }
266
+ ```
267
+
268
+ With `"e": 0` lottie-web loads the file separately and resolves `u + p` relative
269
+ to the page, which breaks as soon as the app is served from a sub-path or
270
+ navigates client-side. Set `options.assetsPath` to the directory where the
271
+ images are deployed instead; react-lottie forwards unknown options to
272
+ lottie-web:
273
+
274
+ ```jsx
275
+ const defaultOptions = {
276
+ path: '/animations/cypher.json',
277
+ assetsPath: '/animations/images/', // trailing slash required
278
+ loop: true,
279
+ autoplay: true,
280
+ };
281
+
282
+ <Lottie options={defaultOptions} height={400} width={400} />
283
+ ```
284
+
285
+ Copy the images next to the JSON in `public/animations/images/` so they are
286
+ served as static files instead of being bundled. `assetsPath` works with both
287
+ `path` and `animationData`, and it applies to every image in the animation.
288
+
289
+ If you want the bundler to hash and inline the images, import them yourself and
290
+ rewrite the asset entries before passing the data. Don't mutate the imported
291
+ JSON in place when it is a frozen ESM object:
292
+
293
+ ```jsx
294
+ import img0 from './images/img_0.png';
295
+ import animationData from './cypher.json';
296
+
297
+ const data = {
298
+ ...animationData,
299
+ assets: animationData.assets.map((asset) => (
300
+ asset.id === 'image_0' ? { ...asset, u: '', p: img0 } : asset
301
+ )),
302
+ };
303
+
304
+ <Lottie options={{ animationData: data, loop: true, autoplay: true }} />
305
+ ```
306
+
307
+ `"e": 1` means the image is embedded in the JSON as a base64 data URI. That
308
+ keeps the animation self-contained, but it inflates the file by roughly a third
309
+ and prevents the browser from caching the image separately, so prefer external
310
+ images for anything large.
311
+
312
+ ### Shrinking the lottie-web bundle
313
+
314
+ react-lottie imports the full `lottie-web` player (299 kB minified, 611 kB
315
+ unminified in lottie-web 5.13), which includes the expression engine and every
316
+ renderer. If your animations don't use expressions and you only need one
317
+ renderer, alias `lottie-web` to a smaller build; react-lottie keeps working
318
+ because those builds expose the same `loadAnimation` API.
319
+
320
+ Vite:
321
+
322
+ ```js
323
+ // vite.config.js
324
+ export default defineConfig({
325
+ resolve: {
326
+ alias: {
327
+ 'lottie-web': 'lottie-web/build/player/lottie_light.js',
328
+ },
329
+ },
330
+ });
331
+ ```
332
+
333
+ webpack (Create React App, Next.js):
334
+
335
+ ```js
336
+ // next.config.js
337
+ module.exports = {
338
+ webpack(config) {
339
+ config.resolve.alias['lottie-web'] = require.resolve(
340
+ 'lottie-web/build/player/lottie_light.js'
341
+ );
342
+ return config;
343
+ },
344
+ };
345
+ ```
346
+
347
+ The light builds are about 45% smaller (164 kB vs 299 kB minified):
348
+
349
+ * `lottie_light.js` — SVG renderer only, no expressions; shapes and raster
350
+ images still work.
351
+ * `lottie_light_canvas.js` — canvas renderer only, no expressions.
352
+ * `lottie_light_html.js` — HTML renderer only, no expressions.
353
+
354
+ Pick the build that matches `options.renderer`: `lottie_light` only registers
355
+ the SVG renderer, so pass `renderer: 'svg'` (the default); use
356
+ `lottie_light_canvas` with `renderer: 'canvas'`. Don't alias when an animation
357
+ uses expressions or effects, because the light builds silently skip them.
358
+
359
+ ### Performance on iOS and low-end devices
360
+
361
+ File size mostly affects how long an animation takes to download and parse, not
362
+ its frame rate. Once playback starts, the cost of each frame comes from what the
363
+ renderer has to draw: the number of layers and shapes, masks and mattes,
364
+ expressions, effects, and the pixel size of the canvas. A multi-megabyte export
365
+ is a good signal that there is a lot of that (issue #133).
366
+
367
+ On iOS 14.5, Safari, Chrome and Firefox all run on WebKit, so they render at the
368
+ same speed; measuring in Safari is representative. Phones are also much slower
369
+ than a desktop, so an animation that is smooth on a laptop can drop frames on
370
+ mobile.
371
+
372
+ In rough order of impact:
373
+
374
+ 1. **Reduce the animation.** Remove hidden and unused layers, flatten
375
+ precompositions, simplify masks and mattes, drop expressions and effects, and
376
+ avoid embedded raster images. If one scene is still heavy, split it into
377
+ smaller files and play only the needed range with `segments`.
378
+ 2. **Draw it at the size it is displayed.** `width` and `height` set the
379
+ container size; rendering a 1200 px animation into a 300 px slot does more
380
+ work than necessary. With the canvas renderer, also cap the backing-store
381
+ resolution: at a device pixel ratio of 3, a 400 px canvas is rasterized at
382
+ 1200 px unless you set `dpr`.
383
+ 3. **Compare the renderers.** `svg` (the default) is usually fastest for a few
384
+ large shapes and stays crisp at any size. `canvas` avoids updating hundreds
385
+ of DOM nodes and often wins for animations with many small shapes. Measure
386
+ both on the target device.
387
+ 4. **Stop off-screen animations.** lottie-web drives every playing animation
388
+ from the same `requestAnimationFrame` loop, so an animation scrolled out of
389
+ view still costs a frame. Pause it with `isPaused`, or with `pause()` on a
390
+ `ref`, driven by an `IntersectionObserver`.
391
+
392
+ ```jsx
393
+ const defaultOptions = {
394
+ animationData,
395
+ loop: true,
396
+ autoplay: true,
397
+ renderer: 'canvas',
398
+ rendererSettings: {
399
+ dpr: 1, // render at CSS resolution; use 2 for a sharper middle ground
400
+ },
401
+ };
402
+
403
+ <Lottie options={defaultOptions} height={300} width={300} />
404
+ ```
405
+
406
+ For the default `svg` renderer, `rendererSettings.progressiveLoad` builds the
407
+ layers over several frames instead of blocking on the first one. It does not
408
+ wait for the element to scroll into view; pause off-screen animations yourself:
409
+
410
+ ```jsx
411
+ // `visible` is kept up to date by an IntersectionObserver on a wrapper element.
412
+ <Lottie
413
+ options={{ animationData, loop: true, autoplay: false, rendererSettings: { progressiveLoad: true } }}
414
+ isPaused={!visible}
415
+ />
416
+ ```
417
+
418
+ If a reduced file still drops frames, profile it: record a timeline in Safari's
419
+ Web Inspector while the animation plays and check whether the time goes to
420
+ lottie-web's frame update or to the browser painting and compositing the layers.
421
+
187
422
  ### Play on hover
188
423
 
189
424
  `eventListeners` takes lottie-web animation events (`complete`, `loopComplete`,
@@ -211,7 +446,7 @@ export default class HoverLottie extends React.Component {
211
446
  const defaultOptions = {
212
447
  loop: false,
213
448
  autoplay: false,
214
- animationData: animationData.default,
449
+ animationData,
215
450
  rendererSettings: {
216
451
  preserveAspectRatio: 'xMidYMid slice'
217
452
  }
@@ -298,6 +533,102 @@ The container is a `div` by default, which is not a valid child of `<p>`. Pass
298
533
  lottie-web should render into. Changing `as` reloads the animation into the new
299
534
  element and keeps the current frame.
300
535
 
536
+ ### Switch between several animations
537
+
538
+ Each `<Lottie>` owns one lottie-web instance, so an app can render two
539
+ animations side by side or keep one animation per page. To choose which one is
540
+ shown, keep the animation data in a map and pass the entry for the current page
541
+ as `options.animationData` (issue #128):
542
+
543
+ ```jsx
544
+ import homeData from './animations/home.json';
545
+ import aboutData from './animations/about.json';
546
+
547
+ const animations = { home: homeData, about: aboutData };
548
+
549
+ const PageAnimation = ({ page }) => (
550
+ <Lottie
551
+ options={{
552
+ loop: true,
553
+ autoplay: true,
554
+ animationData: animations[page],
555
+ }}
556
+ height={300}
557
+ width={300}
558
+ />
559
+ );
560
+ ```
561
+
562
+ Changing `options.animationData` reloads the animation into the same container,
563
+ so a `key` is not required. Keep the JSON modules stable: copying the data in
564
+ `render` (`animationData: { ...animations[page] }`) hands the component a new
565
+ object on every render and reloads the animation each time. If you do want to
566
+ start over — to replay an intro, for example — give the component a `key` so
567
+ React unmounts and remounts it:
568
+
569
+ ```jsx
570
+ <Lottie key={page} options={{ ...defaultOptions, animationData: animations[page] }} />
571
+ ```
572
+
573
+ Unmounting destroys the lottie-web instance, so only the visible page animates.
574
+ When the files are large, load them with `options.path` and fetch the JSON for
575
+ the next page ahead of time instead of shipping every animation in one bundle;
576
+ see [Loading animation data without bloating the bundle](#loading-animation-data-without-bloating-the-bundle).
577
+
578
+ ### Server-side rendering
579
+
580
+ `<Lottie />` renders only a sized container element on the server; lottie-web is
581
+ imported but instantiated in `componentDidMount` only, so `renderToString` (and
582
+ Next.js/Gatsby prerendering) works and the animation starts after hydration:
583
+
584
+ ```jsx
585
+ // server output: <div style="width:400px;height:400px;..."></div>
586
+ <Lottie options={defaultOptions} height={400} width={400} />
587
+ ```
588
+
589
+ Importing the package outside a browser no longer throws
590
+ `ReferenceError: window is not defined`; lottie-web guards its DOM access at
591
+ module load. If your bundler still evaluates the player on the server, load the
592
+ component without SSR:
593
+
594
+ ```jsx
595
+ // Next.js
596
+ import dynamic from 'next/dynamic';
597
+
598
+ const Lottie = dynamic(() => import('react-lottie'), { ssr: false });
599
+ ```
600
+
601
+ Animations loaded from `options.path` are fetched by lottie-web after mount, so
602
+ nothing is requested during the server render. Avoid the hydration trap
603
+ described in [Animations and page navigation](#animations-and-page-navigation)
604
+ when the container is nested in a `<p>`.
605
+
606
+ ### Animations and page navigation
607
+
608
+ react-lottie creates a lottie-web instance when the component mounts and calls
609
+ `destroy()` on unmount, so client-side navigation is safe as long as the page
610
+ really unmounts. A layout that renders every page at once (or keeps pages
611
+ mounted when switching tabs) keeps every animation running; mount only the
612
+ visible one.
613
+
614
+ One non-obvious trap on SSR frameworks such as Gatsby or Next.js: the wrapper is
615
+ a `div` by default, and a `div` inside a `p` is invalid HTML. The browser moves
616
+ the `div` out of the paragraph while parsing the server-rendered page, React
617
+ reports a hydration mismatch (`validateDOMNesting`), and the animation node ends
618
+ up outside the page subtree. Navigating back and forth can then leave animation
619
+ nodes behind. Render the animation with `as="span"` (issue #73) or move it
620
+ outside the paragraph:
621
+
622
+ ```jsx
623
+ <p>
624
+ Text <Lottie as="span" options={defaultOptions} /> more text
625
+ </p>
626
+ ```
627
+
628
+ If animations still accumulate, confirm that each page unmounts (React DevTools
629
+ shows the component tree changing), and pause off-screen instances as described
630
+ in [Performance on iOS and low-end devices](#performance-on-ios-and-low-end-devices).
631
+
301
632
  ### Imperative control
302
633
 
303
634
  A `ref` to the component exposes `play()`, `pause()`, `stop()`,
@@ -329,7 +660,7 @@ the object representing the animation settings that will be instantiated by body
329
660
  >
330
661
  >**path** *optional* — a URL (or path) to a JSON animation file. Mutually exclusive with `animationData`.
331
662
  >
332
- >**renderer** *optional* [default: `svg`]
663
+ >**renderer** *optional* [default: `svg`] — `svg`, `canvas` or `html`. See [Performance on iOS and low-end devices](#performance-on-ios-and-low-end-devices).
333
664
  >
334
665
  >**rendererSettings** *required*
335
666
 
package/dist/index.js CHANGED
@@ -32,6 +32,35 @@ var segmentsAreEqual = function segmentsAreEqual(a, b) {
32
32
  return value === b[index];
33
33
  });
34
34
  };
35
+
36
+ /* lottie-web crashes deep inside its renderer with "Cannot read property
37
+ 'length' of undefined" when the data is not a plain animation object. Accept
38
+ the shapes users actually pass (a namespace import, a frozen ESM object) and
39
+ fail with an actionable message for the rest (#24, #44, #65). */
40
+ var _normalizeAnimationData = function normalizeAnimationData(animationData, path) {
41
+ if (animationData === undefined || animationData === null) {
42
+ if (!path) {
43
+ throw new Error('[react-lottie] options must include `animationData` (the imported JSON) ' + 'or `path` (a URL to the JSON file).');
44
+ }
45
+ return animationData;
46
+ }
47
+ if (typeof animationData === 'string') {
48
+ throw new Error('[react-lottie] options.animationData must be an object, not a string. ' + 'Pass a file path or URL as options.path instead, or call JSON.parse() ' + 'on JSON text first.');
49
+ }
50
+
51
+ /* `import * as animationData` wraps the payload in a `default` property,
52
+ which lottie-web cannot read (#24, #65). */
53
+ if (!Array.isArray(animationData.layers) && animationData["default"]) {
54
+ return _normalizeAnimationData(animationData["default"], path);
55
+ }
56
+
57
+ /* lottie-web annotates the loaded animation (`__complete`), which throws on
58
+ the frozen objects produced by strict ESM bundlers (#24). */
59
+ if (Object.isFrozen(animationData)) {
60
+ return JSON.parse(JSON.stringify(animationData));
61
+ }
62
+ return animationData;
63
+ };
35
64
  var Lottie = exports["default"] = /*#__PURE__*/function (_React$Component) {
36
65
  function Lottie() {
37
66
  var _this;
@@ -71,7 +100,7 @@ var Lottie = exports["default"] = /*#__PURE__*/function (_React$Component) {
71
100
  whether either actually changed. */
72
101
  this.animationData = animationData;
73
102
  this.colorFilters = colorFilters;
74
- var filteredAnimationData = (0, _colorFilters.applyColorFilters)(animationData, colorFilters);
103
+ var filteredAnimationData = (0, _colorFilters.applyColorFilters)(_normalizeAnimationData(animationData, options.path), colorFilters);
75
104
  this.options = {
76
105
  container: this.el,
77
106
  renderer: renderer,
@@ -140,7 +169,7 @@ var Lottie = exports["default"] = /*#__PURE__*/function (_React$Component) {
140
169
  /* `options` is caller-owned and knows nothing about the wrapper, so the
141
170
  ref (already pointing at the newly rendered node) wins. */
142
171
  this.options.container = this.el;
143
- this.options.animationData = (0, _colorFilters.applyColorFilters)(this.props.options.animationData, this.props.colorFilters);
172
+ this.options.animationData = (0, _colorFilters.applyColorFilters)(_normalizeAnimationData(this.props.options.animationData, this.props.options.path), this.props.colorFilters);
144
173
  this.anim = _lottieWeb["default"].loadAnimation(this.options);
145
174
  this.registerEvents(this.props.eventListeners);
146
175
 
@@ -179,7 +208,8 @@ var Lottie = exports["default"] = /*#__PURE__*/function (_React$Component) {
179
208
  value: function componentWillUnmount() {
180
209
  this.deRegisterEvents(this.props.eventListeners);
181
210
  this.destroy();
182
- this.options.animationData = null;
211
+ /* A failed loadAnimation (e.g. invalid animationData) leaves no options. */
212
+ if (this.options) this.options.animationData = null;
183
213
  this.anim = null;
184
214
  }
185
215
  }, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-lottie",
3
- "version": "2.3.1",
3
+ "version": "2.3.3",
4
4
  "funding": "https://opencollective.com/react-lottie",
5
5
  "description": "lottie animation view for React",
6
6
  "repository": {