lottie-react
Animation

Refs and handles

ref reaches the element, lottieRef reaches the animation.

ref on <Lottie> names the element it renders, like on any element. The animation is behind lottieRef:

import { Lottie, type LottieHandle } from "lottie-react";import { useRef } from "react";export function HandleCommands() {  const handle = useRef<LottieHandle>(null);  return (    <>      <Lottie src="/anim.json" loop lottieRef={handle} />      <div>        <button type="button" onClick={() => handle.current?.play()}>          play        </button>        <button type="button" onClick={() => handle.current?.pause()}>          pause        </button>      </div>    </>  );}

lottieRef takes a ref object or a callback and fills it with a LottieHandle. There is no ref type to import: useRef<LottieHandle>(null) is all of it.

What the handle carries

Commands only: play, pause, stop, seek, playSegments, resetSegments, the scrub trio, the setters, reload, and the animationItem escape hatch.

The values are left off on purpose. A ref cannot re-render your component, so a value read from one goes stale silently; reading state from the handle is a compile error instead. subscribe is off it for the same reason subscriptions exist: the prop attaches and cleans up for you, where a ref makes you do both in an effect.

When you need the values

<LottieDisplay> renders from values, so a handle cannot feed it. To place the animation's parts yourself, use the hook and pass the instance.

Commands and signals together

Driving the animation from outside its own tree takes both halves: the handle sends commands, and subscriptions reports back. The handle carries no values on purpose, since anything read through a ref goes stale. When a value is needed, the animation is reached from inside its own tree with useLottieInstance, or built on the hook, where the instance is yours; the section above covers both. One example of the shape, playing the whole file once and then looping one part of it, is a signal followed by a command:

import { Lottie, type LottieHandle } from "lottie-react";import { useRef } from "react";export function PlayOnceThenLoop() {  const handle = useRef<LottieHandle>(null);  const narrowed = useRef(false);  return (    <Lottie      src="/anim.json"      autoplay      loop      lottieRef={handle}      subscriptions={{        loopCompleted: () => {          if (narrowed.current) return;          narrowed.current = true;          handle.current?.playSegments({ marker: "middle" });        },      }}    />  );}

The first pass plays whole. When it wraps, loopCompleted fires and the handler narrows the playable range once; from then on the engine's own loop repeats that range, so nothing has to re-arm. Any signal can drive any command the same way: complete can reset a segment, error can reload(), and marker can seek somewhere else.

On this page