├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .prettierrc ├── .yarnrc.yml ├── LICENSE ├── README.md ├── example ├── README.md ├── favicon.ico ├── index.html ├── manifest.json ├── package.json ├── public │ └── 26514-check-success-animation.json ├── src │ ├── 26514-check-success-animation.json │ ├── App.jsx │ ├── Test.jsx │ ├── index.css │ └── index.jsx ├── vite.config.js └── yarn.lock ├── package.json ├── screenshot.png ├── src ├── LottiePlayer.js ├── LottiePlayer.test.js ├── LottiePlayerLight.d.ts ├── LottiePlayerLight.js ├── __image_snapshots__ │ ├── lottie-player-test-js-lottie-player-screenshots-lottie-player-light-renders-with-animation-data-1-snap.png │ ├── lottie-player-test-js-lottie-player-screenshots-lottie-player-light-renders-with-path-1-snap.png │ ├── lottie-player-test-js-lottie-player-screenshots-lottie-player-main-renders-with-animation-data-1-snap.png │ └── lottie-player-test-js-lottie-player-screenshots-lottie-player-main-renders-with-path-1-snap.png ├── index.d.ts └── makeLottiePlayer.js ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | .snapshots/ 4 | *.min.js 5 | *.d.ts 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['mifi'], 3 | env: { 4 | browser: true, 5 | }, 6 | overrides: [ 7 | { 8 | files: ['**/*.test.js'], 9 | env: { 10 | jest: true, 11 | }, 12 | }, 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v4 11 | 12 | - uses: actions/setup-node@v4 13 | with: 14 | node-version: 20 15 | 16 | - run: corepack enable 17 | 18 | - run: yarn 19 | - run: (cd example && yarn) 20 | 21 | # - uses: mxschmitt/action-tmate@v3 22 | 23 | - run: npm run build 24 | - run: npm run tsc 25 | - run: npm run lint 26 | - run: npm test 27 | - run: npm run predeploy 28 | 29 | - if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' 30 | uses: JamesIves/github-pages-deploy-action@4.1.0 31 | with: 32 | branch: gh-pages 33 | folder: example/dist 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # See https://help.github.com/ignore-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | node_modules 6 | 7 | .yarn/* 8 | !.yarn/releases 9 | !.yarn/plugins 10 | .pnp.* 11 | 12 | example/.yarn/* 13 | !example/.yarn/releases 14 | !example/.yarn/plugins 15 | example/.pnp.* 16 | 17 | # builds 18 | dist 19 | .rpt2_cache 20 | 21 | # misc 22 | .DS_Store 23 | .env 24 | .env.local 25 | .env.development.local 26 | .env.test.local 27 | .env.production.local 28 | 29 | npm-debug.log* 30 | yarn-debug.log* 31 | yarn-error.log* 32 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": true, 4 | "semi": false, 5 | "tabWidth": 2, 6 | "bracketSpacing": true, 7 | "jsxBracketSameLine": false, 8 | "arrowParens": "always", 9 | "trailingComma": "none" 10 | } 11 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Mikael Finstad 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://github.com/mifi/gifs/raw/master/react-lottie-player.gif) 2 | 3 | Fully declarative React Lottie player 4 | 5 | Inspired by [several](https://github.com/felippenardi/lottie-react-web) [existing](https://github.com/chenqingspring/react-lottie) [packages](https://github.com/Gamote/lottie-react) wrapping [lottie-web](https://github.com/airbnb/lottie-web) for React, I created this package because I wanted something that just works and is easy to use. None of the alternatives properly handle changes of props like playing/pausing/segments. This lead to lots of hacks to get the animations to play correctly. 6 | 7 | `react-lottie-player` is a complete rewrite using hooks 🎣 for more readable code, easy to use, seamless and fully declarative control of the lottie player. 8 | 9 | ![Tests](https://github.com/mifi/react-lottie-player/workflows/Tests/badge.svg) [![NPM](https://img.shields.io/npm/v/react-lottie-player.svg)](https://www.npmjs.com/package/react-lottie-player) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) 10 | 11 | ## Features 12 | 13 | - Fully declarative 14 | - Handles state changes correctly 15 | - Does not [leak memory like lottie-web](https://github.com/mifi/react-lottie-player/issues/35) if you use repeaters 16 | - [LottiePlayerLight](#lottieplayerlight) support (no `eval`) 17 | - Alternative imperative API using ref (use at your own risk) 18 | 19 | ## Install 20 | 21 | ```bash 22 | npm install --save react-lottie-player 23 | ``` 24 | 25 | ## Usage 26 | 27 | ```jsx 28 | import React from 'react' 29 | 30 | import Lottie from 'react-lottie-player' 31 | // Alternatively: 32 | // import Lottie from 'react-lottie-player/dist/LottiePlayerLight' 33 | 34 | import lottieJson from './my-lottie.json' 35 | 36 | export default function Example() { 37 | return ( 38 | 44 | ) 45 | } 46 | ``` 47 | 48 | ## Example 49 | 50 | 51 | Screenshot 52 | 53 | 54 | [🎛 Live demo](https://mifi.github.io/react-lottie-player/) 55 | 56 | [👩🏿‍💻 Example code](example/src/App.jsx) 57 | 58 | ## Lazy loading 59 | 60 | ### Option 1: React code splitting (`React.lazy`) 61 | 62 | Extract your Lottie animation into a separate component, then lazy load it: 63 | 64 | ```js 65 | // MyLottieAnimation.jsx 66 | 67 | import Lottie from 'react-lottie-player'; 68 | import animation from './animation.json'; 69 | 70 | export default function MyLottieAnimation(props) { 71 | return ; 72 | } 73 | 74 | // MyComponent.jsx 75 | 76 | import React from 'react'; 77 | const MyLottieAnimation = React.lazy(() => import('./MyLottieAnimation')); 78 | 79 | export default function MyComponent() { 80 | return ; 81 | } 82 | ``` 83 | 84 | ### Option 2: dynamic import with state 85 | 86 | ```js 87 | const Example = () => { 88 | const [animationData, setAnimationData] = useState(); 89 | 90 | useEffect(() => { 91 | import('./animation.json').then(setAnimationData); 92 | }, []); 93 | 94 | if (!animationData) return
Loading...
; 95 | return ; 96 | } 97 | ``` 98 | 99 | ### Option 3: `path` URL 100 | 101 | ```js 102 | const Example = () => ; 103 | ``` 104 | 105 | ## Imperative API (ref) 106 | 107 | ```js 108 | const lottieRef = useRef(); 109 | 110 | useEffect(() => { 111 | console.log(lottieRef.current.currentFrame); 112 | }, []) 113 | 114 | return ; 115 | ``` 116 | 117 | See also [#11](https://github.com/mifi/react-lottie-player/issues/11) 118 | 119 | ## LottiePlayerLight 120 | 121 | The default lottie player uses `eval`. If you don't want eval to be used in your code base, you can instead import `react-lottie-player/dist/LottiePlayerLight`. For more discussion see [#39](https://github.com/mifi/react-lottie-player/pull/39). 122 | 123 | ## Lottie animation track scrolling div 124 | 125 | See [example/App.jsx](example/src/App.jsx) (ScrollTest) in [live example](https://mifi.github.io/react-lottie-player/). 126 | 127 | ## Resize mode: cover 128 | 129 | If you want the animation to fill the whole container, you can pass this prop. See also [#55](https://github.com/mifi/react-lottie-player/issues/55): 130 | 131 | ```js 132 | 133 | ``` 134 | 135 | ## API 136 | 137 | See [type definitions](src/index.d.ts) and [lottie-web](https://github.com/airbnb/lottie-web). 138 | 139 | ## Releasing 140 | 141 | - Commit & wait for CI tests 142 | - `np` 143 | 144 | ## Credits 145 | 146 | - https://lottiefiles.com/26514-check-success-animation 147 | - https://lottiefiles.com/38726-stagger-rainbow 148 | - Published with [create-react-library](https://github.com/transitive-bullshit/create-react-library) 😎 149 | 150 | ## License 151 | 152 | MIT © [mifi](https://github.com/mifi) 153 | 154 | --- 155 | 156 | Made with ❤️ in [🇳🇴](https://www.youtube.com/watch?v=uQIv8Vo9_Jc) 157 | 158 | [More apps by mifi.no](https://mifi.no/) 159 | 160 | Follow me on [GitHub](https://github.com/mifi/), [YouTube](https://www.youtube.com/channel/UC6XlvVH63g0H54HSJubURQA), [IG](https://www.instagram.com/mifi.no/), [Twitter](https://twitter.com/mifi_no) for more awesome content! 161 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | This example is linked to the react-lottie-player package in the parent directory for development purposes. 2 | 3 | You can run `yarn install` and then `yarn start` to test your package. 4 | -------------------------------------------------------------------------------- /example/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mifi/react-lottie-player/fe002b9e67ef663954fc38031882d815e8ffac8a/example/favicon.ico -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 16 | 17 | 18 | react-lottie-player 19 | 20 | 21 | 22 | 25 | 26 |
27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /example/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "react-lottie-player", 3 | "name": "react-lottie-player", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-lottie-player-example", 3 | "homepage": ".", 4 | "version": "0.0.0", 5 | "private": true, 6 | "scripts": { 7 | "start": "node ../node_modules/vite/bin/vite.js", 8 | "build": "node ../node_modules/vite/bin/vite.js build" 9 | }, 10 | "dependencies": { 11 | "react": "link:../node_modules/react", 12 | "react-dom": "link:../node_modules/react-dom", 13 | "react-lottie-player": "link:.." 14 | }, 15 | "browserslist": [ 16 | ">0.2%", 17 | "not dead", 18 | "not ie <= 11", 19 | "not op_mini all" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /example/public/26514-check-success-animation.json: -------------------------------------------------------------------------------- 1 | ../src/26514-check-success-animation.json -------------------------------------------------------------------------------- /example/src/26514-check-success-animation.json: -------------------------------------------------------------------------------- 1 | {"v":"5.6.10","fr":60,"ip":0,"op":108,"w":220,"h":220,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"check","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[109.362,110,0],"ix":2},"a":{"a":0,"k":[46.252,34.491,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":49,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[10,10.5],[-8.75,28.731],[-4.246,34.731],[-2.325,32.76],[14.254,15.75]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":55,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[4.5,14],[-8.75,28.731],[34.504,74.231],[34.604,67.74],[35.004,41.75]],"c":true}]},{"t":62,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[18.875,4.125],[-8.75,28.731],[35.629,74.919],[97.641,10.774],[79.254,-6.25]],"c":true}]}],"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Máscara 1"}],"shapes":[{"ty":"gr","it":[{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Traçado 1","mn":"ADBE Vector Graphic - Stroke","hd":true},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Preenchimento 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Forma 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.806,1.806],[0,0],[-1.806,1.806],[0,0],[-1.806,-1.806],[0,0],[0,0],[-1.806,-1.806],[0,0],[1.806,-1.806],[0,0]],"o":[[0,0],[-1.806,-1.806],[0,0],[1.806,-1.806],[0,0],[0,0],[1.806,-1.806],[0,0],[1.806,1.806],[0,0],[-1.806,1.806]],"v":[[31.418,67.626],[1.355,37.563],[1.355,31.022],[7.895,24.481],[14.436,24.481],[34.689,44.733],[78.067,1.355],[84.608,1.355],[91.149,7.896],[91.149,14.437],[37.959,67.626]],"c":true},"ix":2},"nm":"Caminho 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.439215686275,0.439215686275,0.439215686275,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0.186,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Traçado 1","mn":"ADBE Vector Graphic - Stroke","hd":true},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Preenchimento 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"check","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Preenchimento 1","mn":"ADBE Vector Graphic - Fill","hd":false}],"ip":49,"op":122,"st":-1846,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Elipse 63","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[110,110,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.59,0.59,0.833],"y":[0.941,0.941,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,10.891]},"t":10,"s":[0,0,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,-4.776]},"o":{"x":[0.654,0.654,0.265],"y":[-0.807,-0.807,0]},"t":37,"s":[109.52,109.52,100]},{"t":57,"s":[100,100,100]}],"ix":6,"x":"var $bm_rt;\nvar amp, freq, decay, n, time_max, n, t, t, v;\namp = 0.1;\nfreq = 2;\ndecay = 5;\nn = 0;\n$bm_rt = time_max = 4;\nif (numKeys > 0) {\n $bm_rt = n = nearestKey(time).index;\n if (key(n).time > time) {\n n--;\n }\n}\nif (n == 0) {\n $bm_rt = t = 0;\n} else {\n $bm_rt = t = $bm_sub(time, key(n).time);\n}\nif (n > 0 && t < time_max) {\n v = velocityAtTime($bm_sub(key(n).time, $bm_div(thisComp.frameDuration, 10)));\n $bm_rt = $bm_sum(value, $bm_div($bm_mul($bm_mul(v, amp), Math.sin($bm_mul($bm_mul($bm_mul(freq, t), 2), Math.PI))), Math.exp($bm_mul(decay, t))));\n} else {\n $bm_rt = value;\n}"}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[181,181],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Caminho da elipse 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.439215686275,0.439215686275,0.439215686275,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Traçado 1","mn":"ADBE Vector Graphic - Stroke","hd":true},{"ty":"op","nm":"Deslocar caminhos 1","a":{"a":0,"k":-0.5,"ix":1},"lj":1,"ml":{"a":0,"k":4,"ix":3},"ix":3,"mn":"ADBE Vector Filter - Offset","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Elipse 63 Stroke","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[181,181],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Caminho da elipse 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.125490196078,0.670588235294,0.262745098039,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Preenchimento 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Elipse 63 Fill","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":10,"op":130,"st":10,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Elipse 64","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[110,110,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.59,0.59,0.833],"y":[0.941,0.941,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":5,"s":[0,0,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.654,0.654,0.265],"y":[-0.807,-0.807,0]},"t":32,"s":[109.52,109.52,100]},{"t":52,"s":[100,100,100]}],"ix":6,"x":"var $bm_rt;\nvar amp, freq, decay, n, time_max, n, t, t, v;\namp = 0.1;\nfreq = 2;\ndecay = 5;\nn = 0;\n$bm_rt = time_max = 4;\nif (numKeys > 0) {\n $bm_rt = n = nearestKey(time).index;\n if (key(n).time > time) {\n n--;\n }\n}\nif (n == 0) {\n $bm_rt = t = 0;\n} else {\n $bm_rt = t = $bm_sub(time, key(n).time);\n}\nif (n > 0 && t < time_max) {\n v = velocityAtTime($bm_sub(key(n).time, $bm_div(thisComp.frameDuration, 10)));\n $bm_rt = $bm_sum(value, $bm_div($bm_mul($bm_mul(v, amp), Math.sin($bm_mul($bm_mul($bm_mul(freq, t), 2), Math.PI))), Math.exp($bm_mul(decay, t))));\n} else {\n $bm_rt = value;\n}"}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[181,181],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Caminho da elipse 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.439215686275,0.439215686275,0.439215686275,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Traçado 1","mn":"ADBE Vector Graphic - Stroke","hd":true},{"ty":"op","nm":"Deslocar caminhos 1","a":{"a":0,"k":-0.5,"ix":1},"lj":1,"ml":{"a":0,"k":4,"ix":3},"ix":3,"mn":"ADBE Vector Filter - Offset","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Elipse 63 Stroke","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[181,181],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Caminho da elipse 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.219607843137,0.733333333333,0.333333333333,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Preenchimento 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Elipse 63 Fill","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":5,"op":3605,"st":5,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Elipse 65","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[110,110,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.59,0.59,0.833],"y":[0.941,0.941,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":0,"s":[0,0,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.654,0.654,0.265],"y":[-0.807,-0.807,0]},"t":27,"s":[109.52,109.52,100]},{"t":47,"s":[100,100,100]}],"ix":6,"x":"var $bm_rt;\nvar amp, freq, decay, n, time_max, n, t, t, v;\namp = 0.1;\nfreq = 2;\ndecay = 5;\nn = 0;\n$bm_rt = time_max = 4;\nif (numKeys > 0) {\n $bm_rt = n = nearestKey(time).index;\n if (key(n).time > time) {\n n--;\n }\n}\nif (n == 0) {\n $bm_rt = t = 0;\n} else {\n $bm_rt = t = $bm_sub(time, key(n).time);\n}\nif (n > 0 && t < time_max) {\n v = velocityAtTime($bm_sub(key(n).time, $bm_div(thisComp.frameDuration, 10)));\n $bm_rt = $bm_sum(value, $bm_div($bm_mul($bm_mul(v, amp), Math.sin($bm_mul($bm_mul($bm_mul(freq, t), 2), Math.PI))), Math.exp($bm_mul(decay, t))));\n} else {\n $bm_rt = value;\n}"}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[181,181],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Caminho da elipse 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.439215686275,0.439215686275,0.439215686275,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Traçado 1","mn":"ADBE Vector Graphic - Stroke","hd":true},{"ty":"op","nm":"Deslocar caminhos 1","a":{"a":0,"k":-0.5,"ix":1},"lj":1,"ml":{"a":0,"k":4,"ix":3},"ix":3,"mn":"ADBE Vector Filter - Offset","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Elipse 63 Stroke","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[181,181],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Caminho da elipse 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.337254901961,0.780392156863,0.427450980392,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Preenchimento 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Elipse 63 Fill","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0}],"markers":[]} -------------------------------------------------------------------------------- /example/src/App.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/label-has-associated-control */ 2 | import Lottie from 'react-lottie-player'; 3 | import LottieLight from 'react-lottie-player/dist/LottiePlayerLight.modern'; 4 | 5 | import React, { 6 | useState, memo, useRef, useEffect, 7 | } from 'react'; 8 | import Test from './Test'; 9 | 10 | import lottieJson from './26514-check-success-animation.json'; 11 | 12 | const boxStyle = { 13 | boxShadow: '0 0 10px 10px rgba(0,0,0,0.03)', width: 200, maxWidth: '100%', margin: 30, padding: 30, borderRadius: 7, display: 'flex', flexDirection: 'column', 14 | }; 15 | 16 | const ScrollTest = memo(({ Component, useSubframes }) => { 17 | const scrollRef = useRef(); 18 | const [animationPosition, setAnimationPosition] = useState(0); 19 | 20 | useEffect(() => { 21 | function handleScroll(e) { 22 | setAnimationPosition(Math.max((0, e.target.scrollTop - 50) * 0.3)); 23 | } 24 | const scroller = scrollRef.current; 25 | scroller.addEventListener('scroll', handleScroll, { passive: true }); 26 | 27 | return () => { 28 | scroller.removeEventListener('scroll', handleScroll); 29 | }; 30 | }, []); 31 | 32 | return ( 33 |
34 |
35 |

Scroll down

36 | ⬇️ 37 |
38 | 39 | 47 |
48 | ); 49 | }); 50 | 51 | const MainTest = memo(({ Component, useSubframes }) => { 52 | const [segmentFrom, setSegmentFrom] = useState(0); 53 | const [segmentTo, setSegmentTo] = useState(70); 54 | const [segmentsEnabled, setSegmentsEnabled] = useState(false); 55 | const [play, setPlay] = useState(true); 56 | const [loop, setLoop] = useState(true); 57 | const [loopTimes, setLoopTimes] = useState(0); 58 | const [speed, setSpeed] = useState(1); 59 | const [direction, setDirection] = useState(1); 60 | const segments = [segmentFrom, segmentTo]; 61 | const lottieRef = useRef(); 62 | const logCounter = useRef(0); 63 | 64 | const [log, setLog] = useState([]); 65 | const addLog = (line) => { 66 | logCounter.current += 1; 67 | setLog((existing) => [{ text: line, n: logCounter.current }, ...existing]); 68 | }; 69 | 70 | function handleLoopTimesChange(e) { 71 | const { value } = e.target; 72 | setLoopTimes(value); 73 | const n = parseInt(value, 10); 74 | if (!Number.isInteger(n)) return; 75 | setLoop(n); 76 | } 77 | 78 | function getLoopVal() { 79 | if (loop === true) return ''; 80 | if (loop === false) return 0; 81 | return loopTimes; 82 | } 83 | 84 | useEffect(() => { 85 | // eslint-disable-next-line no-console 86 | console.log('Your lottie object', lottieRef.current); 87 | }, []); 88 | 89 | return ( 90 |
91 | addLog('complete')} 104 | onLoopComplete={() => addLog('loopComplete')} 105 | onEnterFrame={() => { /* addLog('enterFrame') */ }} 106 | onSegmentStart={() => addLog('segmentStart')} 107 | onLoad={() => addLog('load')} 108 | /> 109 | 110 |
111 | setLoop(e.target.checked)} id="loop" /> 112 | {' '} 113 | 114 |
115 | 116 |
117 | Loop times 118 |
119 | 120 |
121 | 122 |
123 | setPlay(e.target.checked)} id="playing1" /> 124 | {' '} 125 | 126 |
127 | 128 |
129 |
130 | setSegmentsEnabled(e.target.checked)} id="segmentsEnabled" /> 131 | 132 |
133 |
134 | Segment from 135 |
136 | setSegmentFrom(parseInt(e.target.value, 10))} /> 137 |
138 |
139 | Segment to 140 |
141 | setSegmentTo(parseInt(e.target.value, 10))} /> 142 |
143 |
144 | 145 |
146 | Speed 147 | setSpeed(parseInt(e.target.value, 10) / 20)} 154 | step="1" 155 | /> 156 |
157 | 158 |
159 | Direction 160 |
161 | setDirection(-1)} /> 162 | setDirection(1)} /> 163 |
164 | 165 |
Event log
166 |
170 | {log.map(({ text, n }) =>
{text}
)} 171 |
172 |
173 | ); 174 | }); 175 | 176 | const RangeTest = memo(({ Component, useSubframes }) => { 177 | const [goTo, setGoTo] = useState(55); 178 | const [play, setPlay] = useState(false); 179 | const [mounted, setMounted] = useState(true); 180 | 181 | return ( 182 |
183 |
184 | setMounted(e.target.checked)} id="mounted1" /> 185 | {' '} 186 | 187 |
188 | 189 | {mounted && ( 190 | <> 191 | 198 | 199 |
200 | setPlay(e.target.checked)} id="playing2" /> 201 | 202 |
203 | 204 |
205 | Controlled position 206 |
207 | setGoTo(parseInt(e.target.value, 10))} 214 | step="1" 215 | /> 216 |
217 | 218 | )} 219 |
220 | ); 221 | }); 222 | 223 | function PathLoadTest({ Component, useSubframes }) { 224 | return ( 225 |
226 | 233 | 234 |

235 | Loaded with 236 | {' '} 237 | path 238 | {' '} 239 | URL 240 |

241 |
242 | ); 243 | } 244 | 245 | const LazyLoadTest = memo(({ Component, useSubframes }) => { 246 | const [animationData, setAnimationData] = useState(); 247 | 248 | useEffect(() => { 249 | setTimeout(() => { 250 | import('./26514-check-success-animation.json').then(setAnimationData); 251 | }, 1000); 252 | }, []); 253 | 254 | return ( 255 |
256 | {animationData ? ( 257 | 264 | ) : ( 265 |
Loading...
266 | )} 267 | 268 |

269 | Lazy loaded with import() 270 |

271 |
272 | ); 273 | }); 274 | 275 | function App() { 276 | const [useLottieLight, setUseLottieLight] = useState(false); 277 | const [useSubframes, setUseSubframes] = useState(); 278 | 279 | const Component = useLottieLight ? LottieLight : Lottie; 280 | 281 | return ( 282 | <> 283 |

react-lottie-player Live Demo

284 |
288 | View source code 289 |
290 | setUseLottieLight(e.target.checked)} /> 291 | {' '} 292 | 293 |
294 |
295 | setUseSubframes(e.target.checked)} /> 296 | {' '} 297 | 298 |
299 |
300 |
301 | 302 | 303 | 304 | 305 | 306 |
307 | 308 | ); 309 | } 310 | 311 | export default window.location.pathname.startsWith('/test') ? Test : App; 312 | -------------------------------------------------------------------------------- /example/src/Test.jsx: -------------------------------------------------------------------------------- 1 | import Lottie from 'react-lottie-player'; 2 | import LottieLight from 'react-lottie-player/dist/LottiePlayerLight.modern'; 3 | 4 | import React from 'react'; 5 | 6 | import lottieJson from './26514-check-success-animation.json'; 7 | 8 | function Test() { 9 | switch (window.location.pathname) { 10 | case '/test/1': return ( 11 | 16 | ); 17 | 18 | case '/test/2': return ( 19 | 24 | ); 25 | 26 | case '/test/3': return ( 27 | 32 | ); 33 | 34 | case '/test/4': return ( 35 | 40 | ); 41 | 42 | default: return null; 43 | } 44 | } 45 | 46 | export default Test; 47 | -------------------------------------------------------------------------------- /example/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 5 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 13 | monospace; 14 | } 15 | -------------------------------------------------------------------------------- /example/src/index.jsx: -------------------------------------------------------------------------------- 1 | import './index.css'; 2 | 3 | import React from 'react'; 4 | import { createRoot } from 'react-dom/client'; 5 | import App from './App'; 6 | 7 | const container = document.getElementById('root'); 8 | const root = createRoot(container); 9 | root.render(); 10 | -------------------------------------------------------------------------------- /example/vite.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | import { defineConfig } from 'vite'; 3 | // eslint-disable-next-line import/no-extraneous-dependencies 4 | import react from '@vitejs/plugin-react'; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [react()], 9 | base: '', 10 | build: { 11 | rollupOptions: { 12 | plugins: [ 13 | // https://github.com/airbnb/lottie-web/issues/2599 14 | { 15 | name: 'disable-treeshake', 16 | transform(code, id) { 17 | if (/node_modules[/\\]lottie-web/.test(id)) { 18 | // Disable tree shake for lottie-web module 19 | return { 20 | code, 21 | map: null, 22 | moduleSideEffects: 'no-treeshake', 23 | }; 24 | } 25 | return null; 26 | }, 27 | }, 28 | ], 29 | }, 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /example/yarn.lock: -------------------------------------------------------------------------------- 1 | # This file is generated by running "yarn install" inside your project. 2 | # Manual changes might be lost - proceed with caution! 3 | 4 | __metadata: 5 | version: 8 6 | cacheKey: 10c0 7 | 8 | "react-dom@link:../node_modules/react-dom::locator=react-lottie-player-example%40workspace%3A.": 9 | version: 0.0.0-use.local 10 | resolution: "react-dom@link:../node_modules/react-dom::locator=react-lottie-player-example%40workspace%3A." 11 | languageName: node 12 | linkType: soft 13 | 14 | "react-lottie-player-example@workspace:.": 15 | version: 0.0.0-use.local 16 | resolution: "react-lottie-player-example@workspace:." 17 | dependencies: 18 | react: "link:../node_modules/react" 19 | react-dom: "link:../node_modules/react-dom" 20 | react-lottie-player: "link:.." 21 | languageName: unknown 22 | linkType: soft 23 | 24 | "react-lottie-player@link:..::locator=react-lottie-player-example%40workspace%3A.": 25 | version: 0.0.0-use.local 26 | resolution: "react-lottie-player@link:..::locator=react-lottie-player-example%40workspace%3A." 27 | languageName: node 28 | linkType: soft 29 | 30 | "react@link:../node_modules/react::locator=react-lottie-player-example%40workspace%3A.": 31 | version: 0.0.0-use.local 32 | resolution: "react@link:../node_modules/react::locator=react-lottie-player-example%40workspace%3A." 33 | languageName: node 34 | linkType: soft 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-lottie-player", 3 | "version": "2.1.0", 4 | "description": "Fully declarative React Lottie player", 5 | "author": "mifi", 6 | "license": "MIT", 7 | "repository": "mifi/react-lottie-player", 8 | "main": "dist/LottiePlayer.js", 9 | "module": "dist/LottiePlayer.modern.js", 10 | "engines": { 11 | "node": ">=10" 12 | }, 13 | "types": "dist/index.d.ts", 14 | "scripts": { 15 | "build": "rm -rf dist && microbundle-crl src/LottiePlayer.js src/LottiePlayerLight.js --no-compress --format modern,cjs -o dist && cp src/*.d.ts dist", 16 | "start": "rm -rf dist && microbundle-crl watch src/LottiePlayer.js src/LottiePlayerLight.js --no-compress --format modern,cjs -o dist", 17 | "test": "npm run build && npm run test:unit", 18 | "lint": "eslint .", 19 | "tsc": "tsc", 20 | "test:unit": "NODE_OPTIONS=--experimental-vm-modules jest", 21 | "predeploy": "cd example && yarn run build", 22 | "deploy": "gh-pages -d example/dist" 23 | }, 24 | "peerDependencies": { 25 | "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" 26 | }, 27 | "devDependencies": { 28 | "@tsconfig/strictest": "^2.0.3", 29 | "@tsconfig/vite-react": "^3.0.1", 30 | "@types/eslint": "^8", 31 | "@types/react": "18", 32 | "@typescript-eslint/eslint-plugin": "^6.12.0", 33 | "@typescript-eslint/parser": "^6.12.0", 34 | "@vitejs/plugin-react": "^4.0.3", 35 | "eslint": "^8.2.0", 36 | "eslint-config-mifi": "^0.0.6", 37 | "eslint-plugin-import": "^2.25.3", 38 | "eslint-plugin-jsx-a11y": "^6.5.1", 39 | "eslint-plugin-react": "^7.28.0", 40 | "eslint-plugin-react-hooks": "^4.3.0", 41 | "eslint-plugin-unicorn": "^51.0.1", 42 | "execa": "^5.0.0", 43 | "gh-pages": "^2.2.0", 44 | "got": "^12.5.3", 45 | "jest": "^29.3.1", 46 | "jest-image-snapshot": "^6.1.0", 47 | "jest-puppeteer": "^6.2.0", 48 | "microbundle-crl": "^0.13.10", 49 | "puppeteer": "^19.4.1", 50 | "react": "^18.2.0", 51 | "react-dom": "^18.2.0", 52 | "typescript": "~5.3.0", 53 | "vite": "^5.1.6" 54 | }, 55 | "files": [ 56 | "dist" 57 | ], 58 | "dependencies": { 59 | "fast-deep-equal": "^3.1.3", 60 | "lottie-web": "^5.12.2", 61 | "rfdc": "^1.3.0" 62 | }, 63 | "jest": { 64 | "preset": "jest-puppeteer" 65 | }, 66 | "packageManager": "yarn@4.0.2" 67 | } 68 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mifi/react-lottie-player/fe002b9e67ef663954fc38031882d815e8ffac8a/screenshot.png -------------------------------------------------------------------------------- /src/LottiePlayer.js: -------------------------------------------------------------------------------- 1 | import lottie from 'lottie-web'; 2 | import makeLottiePlayer from './makeLottiePlayer'; 3 | 4 | export default makeLottiePlayer(lottie); 5 | -------------------------------------------------------------------------------- /src/LottiePlayer.test.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const execa = require('execa'); 3 | // eslint-disable-next-line @typescript-eslint/no-var-requires 4 | const { toMatchImageSnapshot } = require('jest-image-snapshot'); 5 | 6 | beforeAll(() => { 7 | expect.extend({ toMatchImageSnapshot }); 8 | }); 9 | 10 | const port = 5173; 11 | const baseUrl = `http://localhost:${port}`; 12 | 13 | // `wait-on` doesn't seem to work on github actions https://github.com/jeffbski/wait-on/issues/86 14 | // also https://github.com/jeffbski/wait-on/issues/78 15 | async function waitOnPort() { 16 | // eslint-disable-next-line import/no-unresolved 17 | const { got } = await import('got'); 18 | for (;;) { 19 | try { 20 | // eslint-disable-next-line no-await-in-loop 21 | await got(`${baseUrl}/`, { headers: { accept: 'text/html' } }); 22 | return; 23 | // eslint-disable-next-line unicorn/prefer-optional-catch-binding 24 | } catch (err) { 25 | // console.error(err.message); 26 | // retry 27 | // eslint-disable-next-line no-await-in-loop, no-promise-executor-return 28 | await new Promise((resolve) => setTimeout(resolve, 200)); 29 | } 30 | } 31 | } 32 | 33 | describe('lottie player screenshots', () => { 34 | let viteProcess; 35 | 36 | jest.setTimeout(60000); 37 | 38 | beforeAll(async () => { 39 | viteProcess = execa('vite', { cwd: 'example', stderr: 'inherit', stdout: 'inherit' }); 40 | await Promise.race([ 41 | viteProcess, 42 | waitOnPort(), 43 | ]); 44 | }); 45 | 46 | beforeEach(async () => { 47 | await global.jestPuppeteer.resetPage(); 48 | }); 49 | 50 | async function runScreenshotTest(path) { 51 | await global.page.setViewport({ width: 150, height: 150 }); 52 | await global.page.goto(`${baseUrl}${path}`); 53 | await global.page.waitForTimeout(500); // Sometimes global.page is white 54 | const image = await global.page.screenshot(); 55 | 56 | expect(image).toMatchImageSnapshot(); 57 | } 58 | 59 | describe('lottie player main', () => { 60 | it('renders with animationData', async () => { 61 | await runScreenshotTest('/test/1'); 62 | }); 63 | 64 | it('renders with path', async () => { 65 | await runScreenshotTest('/test/2'); 66 | }); 67 | }); 68 | 69 | describe('lottie player light', () => { 70 | it('renders with animationData', async () => { 71 | await runScreenshotTest('/test/3'); 72 | }); 73 | 74 | it('renders with path', async () => { 75 | await runScreenshotTest('/test/4'); 76 | }); 77 | }); 78 | 79 | afterAll(async () => { 80 | viteProcess?.kill?.('SIGINT'); 81 | await viteProcess.catch(() => undefined); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /src/LottiePlayerLight.d.ts: -------------------------------------------------------------------------------- 1 | import '.' 2 | export { default } from 'react-lottie-player' 3 | export * from 'react-lottie-player' 4 | -------------------------------------------------------------------------------- /src/LottiePlayerLight.js: -------------------------------------------------------------------------------- 1 | import lottie from 'lottie-web/build/player/lottie_light'; 2 | import makeLottiePlayer from './makeLottiePlayer'; 3 | 4 | export default makeLottiePlayer(lottie); 5 | -------------------------------------------------------------------------------- /src/__image_snapshots__/lottie-player-test-js-lottie-player-screenshots-lottie-player-light-renders-with-animation-data-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mifi/react-lottie-player/fe002b9e67ef663954fc38031882d815e8ffac8a/src/__image_snapshots__/lottie-player-test-js-lottie-player-screenshots-lottie-player-light-renders-with-animation-data-1-snap.png -------------------------------------------------------------------------------- /src/__image_snapshots__/lottie-player-test-js-lottie-player-screenshots-lottie-player-light-renders-with-path-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mifi/react-lottie-player/fe002b9e67ef663954fc38031882d815e8ffac8a/src/__image_snapshots__/lottie-player-test-js-lottie-player-screenshots-lottie-player-light-renders-with-path-1-snap.png -------------------------------------------------------------------------------- /src/__image_snapshots__/lottie-player-test-js-lottie-player-screenshots-lottie-player-main-renders-with-animation-data-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mifi/react-lottie-player/fe002b9e67ef663954fc38031882d815e8ffac8a/src/__image_snapshots__/lottie-player-test-js-lottie-player-screenshots-lottie-player-main-renders-with-animation-data-1-snap.png -------------------------------------------------------------------------------- /src/__image_snapshots__/lottie-player-test-js-lottie-player-screenshots-lottie-player-main-renders-with-path-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mifi/react-lottie-player/fe002b9e67ef663954fc38031882d815e8ffac8a/src/__image_snapshots__/lottie-player-test-js-lottie-player-screenshots-lottie-player-main-renders-with-path-1-snap.png -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-lottie-player' { 2 | import type { 3 | AnimationConfig, 4 | AnimationDirection, 5 | AnimationEventCallback, 6 | AnimationItem, 7 | AnimationSegment, 8 | RendererType 9 | } from 'lottie-web' 10 | 11 | export type LottieProps = React.PropsWithoutRef, 13 | HTMLDivElement 14 | >> & 15 | Partial, 'loop' | 'renderer' | 'rendererSettings' | 'audioFactory'>> & { 16 | play?: boolean 17 | goTo?: number 18 | speed?: number 19 | direction?: AnimationDirection 20 | segments?: AnimationSegment | AnimationSegment[] 21 | useSubframes?: boolean 22 | 23 | onComplete?: AnimationEventCallback 24 | onLoopComplete?: AnimationEventCallback 25 | onEnterFrame?: AnimationEventCallback 26 | onSegmentStart?: AnimationEventCallback 27 | onLoad?: AnimationEventCallback 28 | 29 | /** Lottie `AnimationItem` Instance */ 30 | ref?: React.Ref 31 | } & ({ path?: string } | { animationData?: { ['default']: object } | object }) 32 | 33 | const Lottie: React.FC 34 | 35 | export default Lottie 36 | } 37 | -------------------------------------------------------------------------------- /src/makeLottiePlayer.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /// 3 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 4 | import React, { 5 | memo, useRef, useEffect, useState, forwardRef, useCallback, 6 | } from 'react'; 7 | import equal from 'fast-deep-equal/es6/react'; 8 | // @ts-expect-error todo 9 | import clone from 'rfdc/default'; 10 | 11 | const emptyObject = {}; 12 | const noOp = () => undefined; 13 | 14 | /** 15 | * @param {import('lottie-web').LottiePlayer} args 16 | * @returns {React.FC} 17 | */ 18 | const makeLottiePlayer = ({ loadAnimation }) => { 19 | const Lottie = memo(forwardRef(( 20 | /** @type {import('react-lottie-player').LottieProps} */ 21 | params, 22 | /** @type {React.ForwardedRef} */ 23 | forwardedRef, 24 | ) => { 25 | const { 26 | play = null, 27 | speed = 1, 28 | direction = 1, 29 | segments: segmentsIn = null, 30 | goTo = null, 31 | useSubframes = true, 32 | 33 | // props picked to match from Lottie's config 34 | renderer = 'svg', 35 | loop = true, 36 | rendererSettings: rendererSettingsIn = emptyObject, 37 | audioFactory, 38 | 39 | onLoad = noOp, 40 | onComplete = noOp, 41 | onLoopComplete = noOp, 42 | onEnterFrame = noOp, 43 | onSegmentStart = noOp, 44 | 45 | // htmlProps remain and will pass on to the div element 46 | ...props 47 | } = params; 48 | 49 | /** @type {import('react-lottie-player').LottieProps} */ 50 | let propsFiltered = props; 51 | /** @type {object | undefined} */ 52 | let animationData; 53 | /** @type {string | undefined} */ 54 | let path; 55 | 56 | if ('animationData' in props) { 57 | ({ animationData, ...propsFiltered } = props); 58 | } 59 | if ('path' in props) { 60 | ({ path, ...propsFiltered } = props); 61 | } 62 | 63 | /** @type {React.MutableRefObject} */ 64 | const animElementRef = useRef(null); 65 | /** @type {React.MutableRefObject} */ 66 | const animRef = useRef(); 67 | 68 | const getAnim = useCallback(() => { 69 | if (animRef.current == null) throw new Error('Lottie ref is not set'); 70 | return animRef.current; 71 | }, []); 72 | 73 | const [ready, setReady] = useState(false); 74 | 75 | const [segments, setSegments] = useState(segmentsIn); 76 | 77 | // Prevent re-init 78 | useEffect(() => { 79 | if (!equal(segments, segmentsIn)) setSegments(segmentsIn); 80 | }, [segmentsIn, segments]); 81 | 82 | const [rendererSettings, setRendererSettings] = useState(rendererSettingsIn); 83 | 84 | // Prevent re-init 85 | useEffect(() => { 86 | if (!equal(rendererSettings, rendererSettingsIn)) setRendererSettings(rendererSettingsIn); 87 | }, [rendererSettingsIn, rendererSettings]); 88 | 89 | // In order to remove listeners before animRef gets destroyed: 90 | useEffect(() => () => getAnim().removeEventListener('complete', onComplete), [getAnim, onComplete]); 91 | useEffect(() => () => getAnim().removeEventListener('loopComplete', onLoopComplete), [getAnim, onLoopComplete]); 92 | useEffect(() => () => getAnim().removeEventListener('enterFrame', onEnterFrame), [getAnim, onEnterFrame]); 93 | useEffect(() => () => getAnim().removeEventListener('segmentStart', onSegmentStart), [getAnim, onSegmentStart]); 94 | useEffect(() => () => getAnim().removeEventListener('DOMLoaded', onLoad), [getAnim, onLoad]); 95 | 96 | const setLottieRefs = useCallback((/** @type {import('lottie-web').AnimationItem | undefined} */ newRef) => { 97 | animRef.current = newRef; 98 | if (typeof forwardedRef === 'function') { 99 | forwardedRef(newRef); 100 | } else if (forwardedRef !== undefined && forwardedRef !== null) { 101 | // eslint-disable-next-line no-param-reassign -- mutating a ref is intended 102 | forwardedRef.current = newRef; 103 | } 104 | }, [forwardedRef]); 105 | 106 | useEffect(() => { 107 | function parseAnimationData() { 108 | if (animationData == null || typeof animationData !== 'object') return animationData; 109 | 110 | // https://github.com/mifi/react-lottie-player/issues/11#issuecomment-879310039 111 | // https://github.com/chenqingspring/vue-lottie/issues/20 112 | if ('default' in animationData && typeof animationData.default === 'object') { 113 | return clone(animationData.default); 114 | } 115 | // cloneDeep to prevent memory leak. See #35 116 | return clone(animationData); 117 | } 118 | 119 | if (animElementRef.current == null) throw new Error('animElementRef is not set'); 120 | 121 | // console.log('init') 122 | const lottie = loadAnimation({ 123 | animationData: parseAnimationData(), 124 | path, 125 | container: animElementRef.current, 126 | renderer, 127 | loop: false, 128 | autoplay: false, // We want to explicitly control playback 129 | rendererSettings, 130 | ...(audioFactory ? { audioFactory } : {}), 131 | }); 132 | setLottieRefs(lottie); 133 | 134 | const onDomLoaded = () => setReady(true); 135 | 136 | getAnim().addEventListener('DOMLoaded', onDomLoaded); 137 | 138 | return () => { 139 | getAnim().removeEventListener('DOMLoaded', onDomLoaded); 140 | setReady(false); 141 | getAnim().destroy(); 142 | setLottieRefs(undefined); 143 | }; 144 | }, [loop, renderer, rendererSettings, animationData, path, audioFactory, setLottieRefs, getAnim]); 145 | 146 | useEffect(() => { 147 | getAnim().addEventListener('DOMLoaded', onLoad); 148 | }, [getAnim, onLoad]); 149 | 150 | useEffect(() => { 151 | getAnim().addEventListener('complete', onComplete); 152 | }, [getAnim, onComplete]); 153 | 154 | useEffect(() => { 155 | getAnim().addEventListener('loopComplete', onLoopComplete); 156 | }, [getAnim, onLoopComplete]); 157 | 158 | useEffect(() => { 159 | getAnim().addEventListener('enterFrame', onEnterFrame); 160 | }, [getAnim, onEnterFrame]); 161 | 162 | useEffect(() => { 163 | getAnim().addEventListener('segmentStart', onSegmentStart); 164 | }, [getAnim, onSegmentStart]); 165 | 166 | useEffect(() => { 167 | if (!ready) return; 168 | getAnim().loop = loop; 169 | }, [ready, loop, getAnim]); 170 | 171 | const wasPlayingSegmentsRef = useRef(false); 172 | 173 | useEffect(() => { 174 | if (!ready) return; 175 | 176 | // Without this code, when playback restarts, it will not play in reverse: 177 | // https://github.com/mifi/react-lottie-player/issues/19 178 | function playReverse(/** @type {number} */ lastFrame) { 179 | getAnim().goToAndPlay(lastFrame, true); 180 | getAnim().setDirection(direction); 181 | } 182 | 183 | if (play === true) { 184 | const force = true; 185 | if (segments) { 186 | getAnim().playSegments(segments, force); 187 | wasPlayingSegmentsRef.current = true; 188 | 189 | // This needs to be called after playSegments or it will not play backwards 190 | if (direction === -1) { 191 | // TODO What if more than one segment 192 | const lastFrame = typeof segments[1] === 'number' ? segments[1] : segments[1][1]; 193 | playReverse(lastFrame); 194 | } 195 | } else { 196 | // If we called playSegments last time, the segments are stored as a state in the lottie object 197 | // Need to reset segments or else it will still play the old segments also when calling play() 198 | // wasPlayingSegmentsRef: Only reset segments if playSegments last time, because resetSegments will also reset playback position 199 | // https://github.com/airbnb/lottie-web/blob/master/index.d.ts 200 | if (wasPlayingSegmentsRef.current) getAnim().resetSegments(force); 201 | wasPlayingSegmentsRef.current = false; 202 | 203 | if (direction === -1) { 204 | const lastFrame = getAnim().getDuration(true); 205 | playReverse(lastFrame); 206 | } else { 207 | getAnim().play(); 208 | } 209 | } 210 | } else if (play === false) { 211 | getAnim().pause(); 212 | } 213 | }, [play, segments, ready, direction, getAnim]); 214 | 215 | useEffect(() => { 216 | if (!ready) return; 217 | if (Number.isNaN(speed)) return; 218 | getAnim().setSpeed(speed); 219 | }, [speed, ready, getAnim]); 220 | 221 | // This handles the case where only direction has changed (direction is not a dependency of the other effect that calls setDirection) 222 | useEffect(() => { 223 | if (!ready) return; 224 | getAnim().setDirection(direction); 225 | }, [direction, getAnim, ready]); 226 | 227 | useEffect(() => { 228 | if (!ready) return; 229 | if (goTo == null) return; 230 | const isFrame = true; // TODO 231 | if (play) getAnim().goToAndPlay(goTo, isFrame); 232 | else getAnim().goToAndStop(goTo, isFrame); 233 | }, [getAnim, goTo, play, ready]); 234 | 235 | useEffect(() => { 236 | if (getAnim().setSubframe) getAnim().setSubframe(useSubframes); 237 | // console.log(getAnim().isSubframeEnabled) 238 | }, [getAnim, useSubframes]); 239 | 240 | return ( 241 | // eslint-disable-next-line react/jsx-filename-extension 242 |
247 | ); 248 | })); 249 | 250 | return Lottie; 251 | }; 252 | 253 | export default makeLottiePlayer; 254 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@tsconfig/strictest", "@tsconfig/vite-react/tsconfig.json"], 3 | "compilerOptions": { 4 | "lib": ["ES2023", "DOM", "DOM.Iterable"], 5 | "noEmit": true, 6 | "checkJs": true, 7 | }, 8 | "include": [ 9 | "src/**/*", 10 | ], 11 | "exclude": [ 12 | "**/*.test.js", 13 | "**/node_modules" 14 | ] 15 | } --------------------------------------------------------------------------------