react-lottie-player and the problem of changing animation props
Fully declarative React Lottie player
At a glance
- What is it?
- react-lottie-player is a React wrapper around lottie-web that treats the animation as declarative state, so props such as play, pause and segment changes are handled without the hacks other wrappers need. The README also advertises a build without eval and an imperative escape hatch for the cases where that model does not fit.
- Who is it for?
- Adopt react-lottie-player when your React app plays Lottie JSON and the animation state has to follow props, and skip it for non-React code or for a strict content security policy unless you import react-lottie-player/dist/LottiePlayerLight.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 7 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The prop change that other Lottie wrappers mishandle
The author started this package because the existing React wrappers for lottie-web did not handle changes of props such as playing, pausing and segments, which led to hacks in application code to make animations behave. That is the problem the library targets, and it is narrower than it first appears. It is not about rendering Lottie files faster or smaller; it is about the gap between a React component and an animation engine that owns its own internal timeline.
A Lottie animation runs on its own frame clock inside lottie-web. A React component, on the other hand, is a function of its props. If a user pauses a video screen or a payment sheet slides in, the component must reconcile the engine's state with the new props. Wrappers that only set values on mount leave the engine running with stale state, and the application ends up calling imperative methods to force things back in line.
The chosen answer is a rewrite using hooks, with the README describing the result as fully declarative control of the lottie player. For the target user that means a frontend team rendering exported After Effects animations as JSON, often in an onboarding flow, a success state or a loading indicator, where the same asset must play, loop, stop or jump to a frame depending on where the user is in the flow.
Declarative props with an imperative escape hatch
The public surface is a set of props. The README example passes loop, animationData, play and a style object, and the component renders the animation. The API section points at the type definitions in src/index.d.ts and at lottie-web for the underlying renderer options, which means the component is a thin mapping rather than a reimplementation of the animation engine.
The interesting part is that the library also keeps the old way in. An imperative API using a ref is supported, and the README is blunt about it: use at your own risk. The documented pattern reads the current frame from the ref.
const lottieRef = useRef();
useEffect(() => {
console.log(lottieRef.current.currentFrame);
}, [])
return <Lottie ref={lottieRef} />;That is a reasonable design, since some interactions, such as reading the playhead at a moment in time, are awkward to express as a prop, and the ref gives you a way to read engine state without rebuilding the whole declarative model. The trade-off is that a project that mixes both styles ends up with two sources of truth about the same animation, and the ref path is where drift between the two usually starts.
Installing the package and rendering your first animation
The project is published on npm as react-lottie-player and the README gives a single install command.
npm install --save react-lottie-playerAfter that, import the default component, point it at a JSON animation you import yourself, and pass the props you want.
import React from 'react'
import Lottie from 'react-lottie-player'
// Alternatively:
// import Lottie from 'react-lottie-player/dist/LottiePlayerLight'
import lottieJson from './my-lottie.json'
export default function Example() {
return (
<Lottie
loop
animationData={lottieJson}
play
style={{ width: 150, height: 150 }}
/>
)
}The animation file is imported as a module, so the JSON is part of your bundle rather than fetched at runtime, and the size prop in the style object controls the rendered box. The alternative import line in the example is not a comment for decoration; it selects a different build, covered later.
Two constraints come from package.json and are worth checking before you install. The peer dependency range is wide, which is good news if you are on a recent React.
"react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"The engine requirement, on the other hand, is old, and it tells you what the package was built to support rather than what it needs today.
"engines": {
"node": ">=10"
},The package ships CommonJS and modern ES module builds plus TypeScript declarations, and the example application in the example/ directory is a Vite project you can read for a fuller usage pattern.
Three ways to bring the animation data in
Loading every animation up front is fine for a small asset and wasteful for a large one, so the README documents three loading strategies. The first is React code splitting with React.lazy. You extract the animation into its own component and then load that component lazily.
// MyLottieAnimation.jsx
import Lottie from 'react-lottie-player';
import animation from './animation.json';
export default function MyLottieAnimation(props) {
return <Lottie animationData={animation} {...props} />;
}The consumer side is a lazy import plus a suspend boundary.
// MyComponent.jsx
import React from 'react';
const MyLottieAnimation = React.lazy(() => import('./MyLottieAnimation'));
export default function MyComponent() {
return <MyLottieAnimation play />;
}Passing props through with the spread operator keeps the wrapper component generic, so the lazy boundary stays out of the way of the play and loop state.
The second strategy loads the JSON with a dynamic import and holds it in state.
const Example = () => {
const [animationData, setAnimationData] = useState<object>();
useEffect(() => {
import('./animation.json').then(setAnimationData);
}, []);
if (!animationData) return <div>Loading...</div>;
return <Lottie animationData={animationData} />;
}Here the component renders a loading placeholder until the data arrives, which is the pattern to use when the animation is not the only thing on screen. The third strategy skips bundling entirely and points the component at a URL.
const Example = () => <Lottie path="https://example.com/lottie.json" />;That path form transfers the request to the runtime, so caching and error handling for the file are yours to arrange, and it is the only one of the three that needs network access to work at all.
The no-eval build and the entry points that produce it
The default player uses eval. The README says plainly that if you do not want eval in your code base you can import a different build instead, and links a pull request for the discussion around it.
The two builds are separate source files, and package.json shows how they are compiled and where they end up.
"build": "rm -rf dist && microbundle-crl src/LottiePlayer.js src/LottiePlayerLight.js --no-compress --format modern,cjs -o dist && cp src/*.d.ts dist",The build produces a modern format and a CommonJS format, disables compression, and copies the hand written type declarations into the output. The package entry points are declared next to each other.
"main": "dist/LottiePlayer.js",
"module": "dist/LottiePlayer.modern.js",
"types": "dist/index.d.ts",Because both builds ship from the same package, switching to the eval-free variant is a one line import change and nothing else in your dependency tree moves. The trade-off is real though: the light player is a reduced renderer, so if your animation relies on a feature that only the full player supports, the light build will not carry it. The README does not enumerate which expressions or effects each build supports, so this is one of the things to check against your own assets rather than assume.
For resizing behaviour, the README documents a cover mode that fills the container by passing renderer settings straight through.
<Lottie rendererSettings={{ preserveAspectRatio: 'xMidYMid slice' }} />The prop name comes from lottie-web, which is the expected shape when the component forwards renderer options rather than reimplementing them.
How the project is built, tested and released
The repository is a library plus a runnable example. The example directory has its own package.json, a Vite config and a manifest, and it is what the live demo page is built from. The top level uses microbundle for the package itself and yarn.lock plus a .yarnrc.yml, while the example has its own yarn.lock, so the two dependency trees are separate.
Testing goes beyond unit level. The dev dependencies include jest with jest-puppeteer and jest-image-snapshot, which is the combination you would expect from a component whose output is pixels. The unit test script sets an experimental flag for the module runtime.
"test:unit": "NODE_OPTIONS=--experimental-vm-modules jest",Image snapshot tests are the interesting part for a renderer: they compare rendered frames against stored images, which catches visual regressions that a plain assertion on props would miss. The test script runs the build first, so the suite exercises the compiled output rather than only the source.
The release process is documented in two steps: commit and wait for CI tests, then run np. That means published versions come from a tagged, tested state rather than from a local build. The MIT licence covers the package, and the repository is not archived. The last push to the default branch was on 2026-10-16, while the newest published release is v2.1.0 from 2024-07-23, ahead of v2.0.0 from 2024-03-13 and v1.5.6 from 2024-01-14. Commits on the branch after the 2024-07-23 release are therefore not in any published version, so pinning the tag gives you the state the maintainer tested, and the difference between the two is worth a look before you assume the branch is the same thing.
Where a React-only Lottie player stops being the right tool
The scope is narrow in ways that matter. This is a React component with a React peer dependency, so it does nothing for a Vue, Svelte or plain JavaScript page, and the README points at lottie-web for the underlying API if you are outside React. The author lists the packages that informed the work, lottie-react-web, react-lottie and Gamote/lottie-react, and explains the difference plainly: they wrap lottie-web for React as well, but none of them handle changes of props such as playing, pausing and segments correctly, which is what forced application level hacks. So the alternative is not a different rendering approach, it is the older imperative style of driving lottie-web by hand from a ref, with the reconciliation work pushed back onto your component.
The second limit is the eval question. The default build evaluates expressions, and the README's answer is a reduced build rather than a configuration switch, so a project with a strict content security policy has to accept the smaller renderer or pick a different component.
The third is memory behaviour with repeaters. The README claims the package does not leak memory the way lottie-web does when you use repeaters, referencing an issue on the subject. That is a claim from the maintainer rather than a measurement you can check here, so if your animations rely heavily on repeaters it deserves a look with a profiler rather than trust. And because the component is a thin layer over lottie-web, the engine's own limits on expression support, renderer features and file size still apply underneath it.
Editorial conclusion
Adopt react-lottie-player when your React app plays Lottie JSON and the animation state has to follow props, and skip it for non-React code or for a strict content security policy unless you import react-lottie-player/dist/LottiePlayerLight. Before wiring it in, read src/index.d.ts to confirm the props you need exist, check that your React version falls inside the declared peer range of 16 through 19, and decide whether to pin v2.1.0, released on 2024-07-23, since the last push to the branch was on 2026-10-16.
Frequently asked questions
How do I install react-lottie-player and play a Lottie JSON file?
Install it with npm install --save react-lottie-player, then import the component and pass the JSON as the animationData prop, along with props such as play or loop. The README example imports the animation with import lottieJson from './my-lottie.json' and sets a width and height in the style prop.
How do I use react-lottie-player without eval in the code base?
Import the light build instead of the default entry point, using react-lottie-player/dist/LottiePlayerLight. The README states that the default lottie player uses eval and points to a pull request for the discussion of the lighter variant.
Can react-lottie-player load a Lottie animation lazily?
Yes. The README documents three options: React.lazy on a component that holds the animation, a dynamic import of the JSON held in component state, and a path prop that points at a JSON URL such as path="https://example.com/lottie.json".
Which React versions does react-lottie-player support?
The peer dependency in package.json lists react as ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0, and the package declares a node engine requirement of >=10.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/mifi-react-lottie-player)Was this useful?