Your designer sent a .lottie file. Now you need it to play on click.
Dropped into the page, it autoplays and loops. What you actually want is for it to play once when someone clicks Submit.
There are three ways to do that, from simplest to most robust.
Setup
DotLottieReact renders a canvas. To control it, you need the player instance, which the component hands you through dotLottieRefCallback.
Storing it in state rather than a ref means your effects re-run once the player exists.
import { useState } from 'react';
import { DotLottieReact } from '@lottiefiles/dotlottie-react';
export function SubmitButton() {
const [player, setPlayer] = useState(null);
return (
<button onClick={() => { player?.stop(); player?.play(); }}>
<DotLottieReact
src="/animations/submit.lottie"
style={{ width: 32, height: 32 }}
dotLottieRefCallback={setPlayer}
/>
Submit
</button>
);
}import { useState } from 'react';
import { DotLottieReact } from '@lottiefiles/dotlottie-react';
export function SubmitButton() {
const [player, setPlayer] = useState(null);
return (
<button onClick={() => { player?.stop(); player?.play(); }}>
<DotLottieReact
src="/animations/submit.lottie"
style={{ width: 32, height: 32 }}
dotLottieRefCallback={setPlayer}
/>
Submit
</button>
);
}import { useState } from 'react';
import { DotLottieReact } from '@lottiefiles/dotlottie-react';
export function SubmitButton() {
const [player, setPlayer] = useState(null);
return (
<button onClick={() => { player?.stop(); player?.play(); }}>
<DotLottieReact
src="/animations/submit.lottie"
style={{ width: 32, height: 32 }}
dotLottieRefCallback={setPlayer}
/>
Submit
</button>
);
}1) play() on click
Leave out autoplay and loop, then call stop() and play() in the click handler.
The stop() matters. It resets to frame 0, so a second click replays the animation instead of doing nothing because it already finished.
Use this when the file holds one animation that always plays start to finish.
2) Named markers
Most button animations have more than one moment: a press, a success, an error. Ask your designer to add a named marker for each one. Then you play a marker by name:
function playMarker(name) {
player?.setMarker(name);
player?.play();
}
ok ? playMarker('success') : playMarker('error');function playMarker(name) {
player?.setMarker(name);
player?.play();
}
ok ? playMarker('success') : playMarker('error');function playMarker(name) {
player?.setMarker(name);
player?.play();
}
ok ? playMarker('success') : playMarker('error');Why markers instead of frame numbers? If the designer retimes the animation, frame 45 might become frame 60 and your code silently plays the wrong part. A marker name survives retiming.
You can list what’s in the file with player.markers().
3) A state machine event
If the animation has real states, like idle, loading and success, that need to blend cleanly, the logic belongs in the file, not in your component.
dotLottie files can carry a state machine. Your code loads it once, then fires events:
import { useEffect } from 'react';
useEffect(() => {
if (!player) return;
const start = () => {
if (player.stateMachineLoad('submitButton')) {
player.stateMachineStart();
}
};
if (player.isLoaded) start();
player.addEventListener('load', start);
return () => player.removeEventListener('load', start);
}, [player]);
player?.stateMachineFireEvent('submit');import { useEffect } from 'react';
useEffect(() => {
if (!player) return;
const start = () => {
if (player.stateMachineLoad('submitButton')) {
player.stateMachineStart();
}
};
if (player.isLoaded) start();
player.addEventListener('load', start);
return () => player.removeEventListener('load', start);
}, [player]);
player?.stateMachineFireEvent('submit');import { useEffect } from 'react';
useEffect(() => {
if (!player) return;
const start = () => {
if (player.stateMachineLoad('submitButton')) {
player.stateMachineStart();
}
};
if (player.isLoaded) start();
player.addEventListener('load', start);
return () => player.removeEventListener('load', start);
}, [player]);
player?.stateMachineFireEvent('submit');Two things trip people up here.
stateMachineLoad only works after the animation has loaded, which is why it runs inside the load listener.
The ID must match the state machine ID in the file exactly. Ask the designer for it rather than guessing.
Gotchas
Size the wrapper. The component fills its container, so a container with no height gives you an invisible animation.
Next.js App Router. The component uses hooks and a canvas, so put it in a file marked 'use client'.
Clean up listeners. Attaching listeners inline in the ref callback adds a new one every time it fires. The effect above removes its listener on cleanup.
What to ask your motion designer for
For option 2, a list of marker names. For option 3, the state machine ID and the names of every event and input.
If those come documented with the file, wiring it up takes minutes, not days.
That handoff note ships with every interactive Lottie project we deliver. If you want animations that arrive ready to wire up, get in touch.