react-lottie 2.1.0 → 2.3.0

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,18 @@
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.0](https://github.com/chenqingspring/react-lottie/compare/v2.2.0...v2.3.0) (2026-11-05)
6
+
7
+ ### Features
8
+
9
+ * add an `as` prop to choose the element rendered around the animation, so `<Lottie as="span" />` can be nested in a `<p>` without invalid HTML. Changing `as` rebinds the animation to the new node and keeps the current frame ([#73](https://github.com/chenqingspring/react-lottie/issues/73))
10
+
11
+ ## [2.2.0](https://github.com/chenqingspring/react-lottie/compare/v2.1.0...v2.2.0) (2026-11-05)
12
+
13
+ ### Features
14
+
15
+ * add a `colorFilters` prop that replaces the colors of fills, strokes and solid layers by keypath, mirroring lottie-react-native's API. The filters are applied to a copy of `animationData`; changing them reloads the animation while preserving the current frame and playback state ([#144](https://github.com/chenqingspring/react-lottie/issues/144))
16
+
5
17
  ## [2.1.0](https://github.com/chenqingspring/react-lottie/compare/v2.0.1...v2.1.0) (2026-11-05)
6
18
 
7
19
  ### Features
package/README.md CHANGED
@@ -116,6 +116,74 @@ export default class LottieControl extends React.Component {
116
116
 
117
117
  ```
118
118
 
119
+ ### Loading animation data without bloating the bundle
120
+
121
+ A static import like `import animationData from './pinjump.json'` (the example
122
+ above) makes the bundler inline the animation into the JavaScript chunk that
123
+ imports it. A 500 kB JSON file then becomes 500 kB of JavaScript the browser has
124
+ to download and parse before the page can run. The JSON is data, not code, so
125
+ keep it out of the main bundle:
126
+
127
+ **Serve it from `public/` and use `options.path`.** In Create React App, Vite
128
+ and Next.js, files in `public/` are copied to the build output as static
129
+ assets; they are not bundled into JavaScript. `lottie-web` fetches the file when
130
+ the component mounts, and the browser caches it separately from the app bundle:
131
+
132
+ ```jsx
133
+ const defaultOptions = {
134
+ path: '/animations/pinjump.json', // relative to the deployed site root
135
+ loop: true,
136
+ autoplay: true,
137
+ rendererSettings: {
138
+ preserveAspectRatio: 'xMidYMid slice'
139
+ }
140
+ };
141
+
142
+ <Lottie options={defaultOptions} height={400} width={400} />
143
+ ```
144
+
145
+ Because lottie-web owns the fetch, `colorFilters` is not applied to animations
146
+ loaded this way — fetch the JSON yourself if you need to recolor it.
147
+
148
+ **Or load it on demand with a dynamic `import()`.** If you need the parsed data
149
+ in your own code, a dynamic import puts the JSON in a separate chunk that is
150
+ downloaded only when the component renders:
151
+
152
+ ```jsx
153
+ import { useEffect, useState } from 'react';
154
+ import Lottie from 'react-lottie';
155
+
156
+ export default function Animation() {
157
+ const [animationData, setAnimationData] = useState(null);
158
+
159
+ useEffect(() => {
160
+ let cancelled = false;
161
+ import('./pinjump.json').then(({ default: data }) => {
162
+ if (!cancelled) setAnimationData(data);
163
+ });
164
+ return () => { cancelled = true; };
165
+ }, []);
166
+
167
+ if (!animationData) return null;
168
+
169
+ return (
170
+ <Lottie
171
+ options={{ animationData, loop: true, autoplay: true }}
172
+ height={400}
173
+ width={400}
174
+ />
175
+ );
176
+ }
177
+ ```
178
+
179
+ Rendering `<Animation />` lazily (for example when it scrolls into view) defers
180
+ even that chunk until it is needed. The same idea works with `React.lazy` around
181
+ a component that imports the JSON.
182
+
183
+ Finally, the file itself can usually get smaller: export at a lower precision,
184
+ remove unused layers and precompositions, avoid embedded raster images, and let
185
+ gzip/Brotli compress the JSON. LottieFiles' optimizer automates the first steps.
186
+
119
187
  ### Play on hover
120
188
 
121
189
  `eventListeners` takes lottie-web animation events (`complete`, `loopComplete`,
@@ -187,6 +255,49 @@ A segment also has to be reachable with `loop`/`autoplay` as usual: with
187
255
  `loop: false` and `autoplay: false` the range only plays once you flip
188
256
  `isStopped` or pass a new `segments` value.
189
257
 
258
+ ### Recolor an animation
259
+
260
+ `colorFilters` replaces the colors of fills, strokes and solid layers named by
261
+ a keypath, so one animation file can follow a theme (issue #144):
262
+
263
+ ```jsx
264
+ <Lottie
265
+ options={defaultOptions}
266
+ colorFilters={[
267
+ { keypath: 'Pin 1.Shape 2.Fill 1', color: '#4f46e5' },
268
+ { keypath: 'Background', color: 'rgb(15, 23, 42)' },
269
+ ]}
270
+ />
271
+ ```
272
+
273
+ A keypath is the dot-separated name chain from the layer down to the fill,
274
+ stroke or solid, e.g. `layer.group.fill`. For a layer inside a precomposition,
275
+ the composition name can be included (`Precomp.InnerLayer.Shape.Fill 1`) or
276
+ omitted. `*` matches one level and `**` matches any number of levels; dots in
277
+ a name can be escaped with a backslash. Colors accept hex, `rgb()`/`rgba()`,
278
+ `hsl()`/`hsla()` and the basic CSS color names. Invalid filters are ignored.
279
+
280
+ The filters are applied to `options.animationData`. Changing them reloads the
281
+ animation, because lottie-web cannot recolor a loaded animation; the current
282
+ frame and paused/playing state are preserved. Animations loaded from
283
+ `options.path` are not recolored, since the JSON is fetched by lottie-web:
284
+ load the JSON yourself and pass it as `animationData` instead.
285
+
286
+ ### Render inline inside a paragraph
287
+
288
+ The container is a `div` by default, which is not a valid child of `<p>`. Pass
289
+ `as` to choose the element (issue #73):
290
+
291
+ ```jsx
292
+ <p>
293
+ Text <Lottie as="span" options={defaultOptions} ariaRole="img" /> more text
294
+ </p>
295
+ ```
296
+
297
+ `as` accepts a tag name or a component that forwards its `ref` to the DOM node
298
+ lottie-web should render into. Changing `as` reloads the animation into the new
299
+ element and keeps the current frame.
300
+
190
301
  ### Imperative control
191
302
 
192
303
  A `ref` to the component exposes `play()`, `pause()`, `stop()`,
@@ -222,6 +333,11 @@ the object representing the animation settings that will be instantiated by body
222
333
  >
223
334
  >**rendererSettings** *required*
224
335
 
336
+ **colorFilters** *optional* [default: `[]`]
337
+
338
+ Array of `{ keypath, color }` objects that replace the colors of fills,
339
+ strokes and solid layers. See [Recolor an animation](#recolor-an-animation).
340
+
225
341
  **width** *optional* [default: `100%`]
226
342
 
227
343
  pixel value for containers width.
@@ -303,6 +419,13 @@ layout defaults. Use it to override `overflow` or `margin`.
303
419
  Tab order of the rendered container. Pass `-1` for a purely presentational
304
420
  animation that should not receive keyboard focus.
305
421
 
422
+ **as** *optional* [default: `div`]
423
+
424
+ Element or component rendered as the animation container. Use `as="span"` when
425
+ `<Lottie />` is a child of `<p>`, where a `div` is invalid HTML (issue #73). A
426
+ custom component must forward its `ref` to a DOM element. See
427
+ [Render inline inside a paragraph](#render-inline-inside-a-paragraph).
428
+
306
429
  ## Related Projects
307
430
 
308
431
  * [lottie-web](https://github.com/airbnb/lottie-web) react-lottie is a wrapper of lottie-web
@@ -0,0 +1,260 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.parseColor = exports.colorFiltersAreEqual = exports.applyColorFilters = void 0;
7
+ function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); }
8
+ function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
9
+ function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
10
+ function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); }
11
+ function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); }
12
+ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
13
+ function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
14
+ /* `colorFilters` mirrors lottie-react-native: every filter names a keypath in
15
+ the animation (layer → group → shape item) and the color to replace there.
16
+ lottie-web has no public API for recoloring a loaded animation, so the
17
+ animation JSON is cloned with the matching colors replaced and the animation
18
+ is loaded again. The caller's animationData is never mutated, because it may
19
+ be a frozen ESM import (#144). */
20
+
21
+ var NAMED_COLORS = {
22
+ aqua: [0, 255, 255],
23
+ black: [0, 0, 0],
24
+ blue: [0, 0, 255],
25
+ cyan: [0, 255, 255],
26
+ fuchsia: [255, 0, 255],
27
+ gray: [128, 128, 128],
28
+ green: [0, 128, 0],
29
+ grey: [128, 128, 128],
30
+ lime: [0, 255, 0],
31
+ magenta: [255, 0, 255],
32
+ maroon: [128, 0, 0],
33
+ navy: [0, 0, 128],
34
+ olive: [128, 128, 0],
35
+ purple: [128, 0, 128],
36
+ red: [255, 0, 0],
37
+ silver: [192, 192, 192],
38
+ teal: [0, 128, 128],
39
+ white: [255, 255, 255],
40
+ yellow: [255, 255, 0]
41
+ };
42
+ var clampChannel = function clampChannel(value) {
43
+ return Math.min(255, Math.max(0, value));
44
+ };
45
+ var toFractions = function toFractions(channels) {
46
+ return channels.map(function (channel) {
47
+ return clampChannel(channel) / 255;
48
+ });
49
+ };
50
+ var parseHex = function parseHex(value) {
51
+ var match = /^#?([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(value);
52
+ if (!match) return null;
53
+ var digits = match[1].length <= 4 ? match[1].split('').map(function (digit) {
54
+ return digit + digit;
55
+ }) : match[1].match(/.{2}/g);
56
+
57
+ /* The fourth value is alpha; lottie keeps the animation's own alpha, so it
58
+ is ignored here. */
59
+ return toFractions(digits.slice(0, 3).map(function (digit) {
60
+ return parseInt(digit, 16);
61
+ }));
62
+ };
63
+ var parseChannel = function parseChannel(token) {
64
+ var value = parseFloat(token);
65
+ if (Number.isNaN(value)) return null;
66
+ return token.endsWith('%') ? value * 255 / 100 : value;
67
+ };
68
+ var parseRgb = function parseRgb(value) {
69
+ var match = /^rgba?\(\s*([^)]+)\)$/i.exec(value);
70
+ if (!match) return null;
71
+ var channels = match[1].split(/[\s,/]+/).filter(Boolean).slice(0, 3).map(parseChannel);
72
+ if (channels.length !== 3 || channels.some(function (channel) {
73
+ return channel === null;
74
+ })) return null;
75
+ return toFractions(channels);
76
+ };
77
+ var hueToRgb = function hueToRgb(p, q, t) {
78
+ var hue = t;
79
+ if (hue < 0) hue += 1;
80
+ if (hue > 1) hue -= 1;
81
+ if (hue < 1 / 6) return p + (q - p) * 6 * hue;
82
+ if (hue < 1 / 2) return q;
83
+ if (hue < 2 / 3) return p + (q - p) * (2 / 3 - hue) * 6;
84
+ return p;
85
+ };
86
+ var parseHsl = function parseHsl(value) {
87
+ var match = /^hsla?\(\s*([^)]+)\)$/i.exec(value);
88
+ if (!match) return null;
89
+ var parts = match[1].split(/[\s,/]+/).filter(Boolean);
90
+ if (parts.length < 3) return null;
91
+ var hue = (parseFloat(parts[0]) % 360 + 360) % 360 / 360;
92
+ var saturation = parseFloat(parts[1]) / 100;
93
+ var lightness = parseFloat(parts[2]) / 100;
94
+ if (Number.isNaN(hue) || Number.isNaN(saturation) || Number.isNaN(lightness)) {
95
+ return null;
96
+ }
97
+ if (saturation === 0) return [lightness, lightness, lightness];
98
+ var q = lightness < 0.5 ? lightness * (1 + saturation) : lightness + saturation - lightness * saturation;
99
+ var p = 2 * lightness - q;
100
+ return [hueToRgb(p, q, hue + 1 / 3), hueToRgb(p, q, hue), hueToRgb(p, q, hue - 1 / 3)];
101
+ };
102
+
103
+ /* Accepts the CSS color forms an app is likely to use for theming. Anything
104
+ else (including CSS variables) is ignored rather than crashing the render. */
105
+ var parseColor = exports.parseColor = function parseColor(value) {
106
+ if (typeof value !== 'string') return null;
107
+ var color = value.trim().toLowerCase();
108
+ if (NAMED_COLORS[color]) return toFractions(NAMED_COLORS[color]);
109
+ return parseHex(color) || parseRgb(color) || parseHsl(color);
110
+ };
111
+ var setColorArray = function setColorArray(target, color) {
112
+ if (!Array.isArray(target) || target.length < 3) return false;
113
+ target[0] = color[0];
114
+ target[1] = color[1];
115
+ target[2] = color[2];
116
+ return true;
117
+ };
118
+
119
+ /* A color property is either static (`k: [r, g, b, a]`) or animated
120
+ (`k: [{ s: [...], e: [...] }, ...]`). Replace both so an animated fill stays
121
+ recolored throughout its keyframes. */
122
+ var setColorProperty = function setColorProperty(property, color) {
123
+ if (!property || !Array.isArray(property.k) || property.k.length === 0) return false;
124
+ if (typeof property.k[0] === 'number') {
125
+ return setColorArray(property.k, color);
126
+ }
127
+ var applied = false;
128
+ property.k.forEach(function (keyframe) {
129
+ if (!keyframe || _typeof(keyframe) !== 'object') return;
130
+ if (setColorArray(keyframe.s, color)) applied = true;
131
+ if (setColorArray(keyframe.e, color)) applied = true;
132
+ });
133
+ return applied;
134
+ };
135
+ var toHex = function toHex(color) {
136
+ return "#".concat(color.map(function (channel) {
137
+ return Math.round(channel * 255).toString(16).padStart(2, '0');
138
+ }).join(''));
139
+ };
140
+
141
+ /* Solid layers store their color as a hex string instead of a color array. */
142
+ var setSolidColor = function setSolidColor(layer, color) {
143
+ if (typeof layer.sc === 'string') {
144
+ layer.sc = toHex(color);
145
+ return true;
146
+ }
147
+ return setColorArray(layer.sc, color);
148
+ };
149
+
150
+ /* Split on unescaped dots so layer names that contain a dot (`icon.dark`) can
151
+ be targeted with `icon\.dark`. */
152
+ var splitKeypath = function splitKeypath(keypath) {
153
+ var parts = [];
154
+ var current = '';
155
+ for (var index = 0; index < keypath.length; index += 1) {
156
+ var character = keypath[index];
157
+ if (character === '\\' && index + 1 < keypath.length) {
158
+ current += keypath[index + 1];
159
+ index += 1;
160
+ } else if (character === '.') {
161
+ parts.push(current);
162
+ current = '';
163
+ } else {
164
+ current += character;
165
+ }
166
+ }
167
+ parts.push(current);
168
+ return parts;
169
+ };
170
+
171
+ /* `*` matches one level and `**` matches any number of levels, matching the
172
+ keypath conventions used by lottie-ios. */
173
+ var _matchesKeypath = function matchesKeypath(pattern, path) {
174
+ if (pattern.length === 0) return path.length === 0;
175
+ if (pattern[0] === '**') {
176
+ for (var index = 0; index <= path.length; index += 1) {
177
+ if (_matchesKeypath(pattern.slice(1), path.slice(index))) return true;
178
+ }
179
+ return false;
180
+ }
181
+ if (path.length === 0) return false;
182
+ if (pattern[0] !== '*' && pattern[0] !== path[0]) return false;
183
+ return _matchesKeypath(pattern.slice(1), path.slice(1));
184
+ };
185
+ var colorFiltersAreEqual = exports.colorFiltersAreEqual = function colorFiltersAreEqual(first, second) {
186
+ if (first === second) return true;
187
+ if (!Array.isArray(first) || !Array.isArray(second) || first.length !== second.length) {
188
+ return false;
189
+ }
190
+ return first.every(function (filter, index) {
191
+ var other = second[index];
192
+ return Boolean(filter && other) && filter.keypath === other.keypath && filter.color === other.color;
193
+ });
194
+ };
195
+ var applyColorFilters = exports.applyColorFilters = function applyColorFilters(animationData, colorFilters) {
196
+ if (!animationData || !Array.isArray(colorFilters) || colorFilters.length === 0) {
197
+ return animationData;
198
+ }
199
+ var filters = colorFilters.filter(function (filter) {
200
+ return filter && typeof filter.keypath === 'string';
201
+ }).map(function (filter) {
202
+ return {
203
+ parts: splitKeypath(filter.keypath.trim()),
204
+ color: parseColor(filter.color)
205
+ };
206
+ }).filter(function (filter) {
207
+ return filter.color;
208
+ });
209
+ if (filters.length === 0) return animationData;
210
+ var data = JSON.parse(JSON.stringify(animationData));
211
+ var assets = new Map();
212
+ if (Array.isArray(data.assets)) {
213
+ data.assets.forEach(function (asset) {
214
+ if (asset && asset.id) assets.set(asset.id, asset);
215
+ });
216
+ }
217
+ var applied = false;
218
+ var recolor = function recolor(node, path) {
219
+ filters.forEach(function (filter) {
220
+ if (!_matchesKeypath(filter.parts, path)) return;
221
+ if (node.c && setColorProperty(node.c, filter.color)) applied = true;
222
+ if (node.ty === 1 && setSolidColor(node, filter.color)) applied = true;
223
+ });
224
+ };
225
+ var _walkShapes = function walkShapes(shapes, path) {
226
+ if (!Array.isArray(shapes)) return;
227
+ shapes.forEach(function (shape) {
228
+ if (!shape || _typeof(shape) !== 'object') return;
229
+ var shapePath = shape.nm ? [].concat(_toConsumableArray(path), [shape.nm]) : path;
230
+ recolor(shape, shapePath);
231
+ _walkShapes(shape.it, shapePath);
232
+ _walkShapes(shape.shapes, shapePath);
233
+ });
234
+ };
235
+
236
+ /* Precompositions are walked twice: once from the asset (so a keypath can
237
+ start at the inner layer name) and once from each layer that references
238
+ them (so `Precomp.InnerLayer...` also works). `refStack` prevents a
239
+ self-referencing composition from recursing forever. */
240
+ var _walkLayers = function walkLayers(layers, prefix, refStack) {
241
+ if (!Array.isArray(layers)) return;
242
+ layers.forEach(function (layer) {
243
+ if (!layer || _typeof(layer) !== 'object') return;
244
+ var path = layer.nm ? [].concat(_toConsumableArray(prefix), [layer.nm]) : prefix;
245
+ recolor(layer, path);
246
+ _walkShapes(layer.shapes, path);
247
+ var asset = layer.refId ? assets.get(layer.refId) : null;
248
+ if (layer.ty === 0 && asset && !refStack.includes(layer.refId)) {
249
+ _walkLayers(asset.layers, path, [].concat(_toConsumableArray(refStack), [layer.refId]));
250
+ }
251
+ });
252
+ };
253
+ _walkLayers(data.layers, [], []);
254
+ if (Array.isArray(data.assets)) {
255
+ data.assets.forEach(function (asset) {
256
+ return _walkLayers(asset.layers, [], []);
257
+ });
258
+ }
259
+ return applied ? data : animationData;
260
+ };
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ exports["default"] = void 0;
7
7
  var _react = _interopRequireDefault(require("react"));
8
8
  var _propTypes = _interopRequireDefault(require("prop-types"));
9
9
  var _lottieWeb = _interopRequireDefault(require("lottie-web"));
10
+ var _colorFilters = require("./colorFilters");
10
11
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { "default": e }; }
11
12
  function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
12
13
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
@@ -56,7 +57,8 @@ var Lottie = exports["default"] = /*#__PURE__*/function (_React$Component) {
56
57
  value: function componentDidMount() {
57
58
  var _this$props = this.props,
58
59
  options = _this$props.options,
59
- eventListeners = _this$props.eventListeners;
60
+ eventListeners = _this$props.eventListeners,
61
+ colorFilters = _this$props.colorFilters;
60
62
  var loop = options.loop,
61
63
  autoplay = options.autoplay,
62
64
  animationData = options.animationData,
@@ -64,16 +66,28 @@ var Lottie = exports["default"] = /*#__PURE__*/function (_React$Component) {
64
66
  renderer = _options$renderer === void 0 ? 'svg' : _options$renderer,
65
67
  rendererSettings = options.rendererSettings,
66
68
  segments = options.segments;
69
+
70
+ /* Keep the caller's data and filter list around so a later update can tell
71
+ whether either actually changed. */
72
+ this.animationData = animationData;
73
+ this.colorFilters = colorFilters;
74
+ var filteredAnimationData = (0, _colorFilters.applyColorFilters)(animationData, colorFilters);
67
75
  this.options = {
68
76
  container: this.el,
69
77
  renderer: renderer,
70
78
  loop: loop !== false,
71
79
  autoplay: autoplay !== false,
72
80
  segments: segments !== false,
73
- animationData: animationData,
81
+ animationData: filteredAnimationData,
74
82
  rendererSettings: rendererSettings
75
83
  };
76
84
  this.options = _objectSpread(_objectSpread({}, this.options), options);
85
+ /* Spreading `options` restored the unfiltered data; use the filtered copy
86
+ after the merge. */
87
+ this.options.animationData = filteredAnimationData;
88
+ if (typeof process !== 'undefined' && process.env.NODE_ENV !== 'production' && Array.isArray(colorFilters) && colorFilters.length > 0 && !animationData) {
89
+ console.warn('[react-lottie] colorFilters requires options.animationData; ' + 'animations loaded from options.path are not recolored.');
90
+ }
77
91
  this.anim = _lottieWeb["default"].loadAnimation(this.options);
78
92
  this.registerEvents(eventListeners);
79
93
  this.setSpeed();
@@ -101,17 +115,42 @@ var Lottie = exports["default"] = /*#__PURE__*/function (_React$Component) {
101
115
  }, {
102
116
  key: "componentDidUpdate",
103
117
  value: function componentDidUpdate(prevProps) {
104
- /* Recreate the animation handle if the data or its remote path changed.
105
- Done here rather than in componentWillUpdate, which React has deprecated
106
- and which is unsafe in concurrent rendering (#77, #100, #119). */
107
- var dataChanged = this.options.animationData !== this.props.options.animationData;
118
+ /* Recreate the animation handle if the data, its remote path or the color
119
+ filters changed. Done here rather than in componentWillUpdate, which
120
+ React has deprecated and which is unsafe in concurrent rendering
121
+ (#77, #100, #119). */
122
+ var dataChanged = this.animationData !== this.props.options.animationData;
108
123
  var pathChanged = this.options.path !== this.props.options.path;
109
- if (dataChanged || pathChanged) {
124
+ var colorFiltersChanged = !(0, _colorFilters.colorFiltersAreEqual)(this.colorFilters, this.props.colorFilters);
125
+ /* The wrapper element is part of lottie-web's configuration, so a new
126
+ element means the animation has to be bound to the new node. */
127
+ var containerChanged = prevProps.as !== this.props.as;
128
+ if (dataChanged || pathChanged || colorFiltersChanged || containerChanged) {
129
+ var currentFrame = this.anim ? this.anim.currentFrame : 0;
130
+ var wasPaused = this.anim ? this.anim.isPaused : true;
131
+ /* lottie-web cannot recolor a loaded animation and cannot move one to
132
+ another node, so those changes load the same data again. Only then is
133
+ it worth restoring the frame and playback state. */
134
+ var reloadInPlace = !dataChanged && !pathChanged;
110
135
  this.deRegisterEvents(prevProps.eventListeners);
111
136
  this.destroy();
137
+ this.animationData = this.props.options.animationData;
138
+ this.colorFilters = this.props.colorFilters;
112
139
  this.options = _objectSpread(_objectSpread({}, this.options), this.props.options);
140
+ /* `options` is caller-owned and knows nothing about the wrapper, so the
141
+ ref (already pointing at the newly rendered node) wins. */
142
+ this.options.container = this.el;
143
+ this.options.animationData = (0, _colorFilters.applyColorFilters)(this.props.options.animationData, this.props.colorFilters);
113
144
  this.anim = _lottieWeb["default"].loadAnimation(this.options);
114
145
  this.registerEvents(this.props.eventListeners);
146
+
147
+ /* Keep the frame and playback state so a theme or element change does
148
+ not restart a paused or half-played animation. */
149
+ if (reloadInPlace) {
150
+ if (this.props.segments) this.anim.playSegments(this.props.segments, true);
151
+ if (Number.isFinite(currentFrame)) this.anim.goToAndStop(currentFrame, true);
152
+ if (!wasPaused) this.anim.play();
153
+ }
115
154
  }
116
155
 
117
156
  /* Only drive playback when a playback prop asks for it. Re-rendering used
@@ -232,7 +271,8 @@ var Lottie = exports["default"] = /*#__PURE__*/function (_React$Component) {
232
271
  ariaLabel = _this$props2.ariaLabel,
233
272
  isClickToPauseDisabled = _this$props2.isClickToPauseDisabled,
234
273
  title = _this$props2.title,
235
- tabIndex = _this$props2.tabIndex;
274
+ tabIndex = _this$props2.tabIndex,
275
+ Container = _this$props2.as;
236
276
  var getSize = function getSize(initial) {
237
277
  var size;
238
278
  if (typeof initial === 'number') {
@@ -257,7 +297,7 @@ var Lottie = exports["default"] = /*#__PURE__*/function (_React$Component) {
257
297
  // Click-to-pause is intentional and role="button" is set; keyboard control
258
298
  // is not part of the API yet.
259
299
  // eslint-disable-next-line jsx-a11y/click-events-have-key-events
260
- _react["default"].createElement("div", {
300
+ _react["default"].createElement(Container, {
261
301
  ref: function ref(c) {
262
302
  _this4.el = c;
263
303
  },
@@ -275,6 +315,10 @@ var Lottie = exports["default"] = /*#__PURE__*/function (_React$Component) {
275
315
  Lottie.propTypes = {
276
316
  eventListeners: _propTypes["default"].arrayOf(_propTypes["default"].object),
277
317
  options: _propTypes["default"].object.isRequired,
318
+ colorFilters: _propTypes["default"].arrayOf(_propTypes["default"].shape({
319
+ keypath: _propTypes["default"].string.isRequired,
320
+ color: _propTypes["default"].string.isRequired
321
+ })),
278
322
  height: _propTypes["default"].oneOfType([_propTypes["default"].string, _propTypes["default"].number]),
279
323
  width: _propTypes["default"].oneOfType([_propTypes["default"].string, _propTypes["default"].number]),
280
324
  isStopped: _propTypes["default"].bool,
@@ -288,10 +332,12 @@ Lottie.propTypes = {
288
332
  isClickToPauseDisabled: _propTypes["default"].bool,
289
333
  title: _propTypes["default"].string,
290
334
  style: _propTypes["default"].object,
291
- tabIndex: _propTypes["default"].number
335
+ tabIndex: _propTypes["default"].number,
336
+ as: _propTypes["default"].elementType
292
337
  };
293
338
  Lottie.defaultProps = {
294
339
  eventListeners: [],
340
+ colorFilters: [],
295
341
  isStopped: false,
296
342
  isPaused: false,
297
343
  speed: 1,
@@ -300,5 +346,6 @@ Lottie.defaultProps = {
300
346
  ariaLabel: 'animation',
301
347
  isClickToPauseDisabled: false,
302
348
  title: '',
303
- tabIndex: 0
349
+ tabIndex: 0,
350
+ as: 'div'
304
351
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-lottie",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "funding": "https://opencollective.com/react-lottie",
5
5
  "description": "lottie animation view for React",
6
6
  "repository": {
@@ -51,7 +51,8 @@
51
51
  "react": ">=16.8.0"
52
52
  },
53
53
  "overrides": {
54
- "cheerio": "1.0.0-rc.12"
54
+ "cheerio": "1.0.0-rc.12",
55
+ "minimatch@^10.2.2": "11.0.4"
55
56
  },
56
57
  "resolutions": {
57
58
  "cheerio": "1.0.0-rc.12"