ReactでLottieのアニメーションJSONを表示する方法

​​React​Lottie,lottie-web,TypeScript

Lottie のアニメーション(JSON)は、lottie-react の <Lottie> に JSON を渡すだけで React に表示できます。autoplay と loop を付けると、読み込んだらすぐに再生が始まり、繰り返し再生されます。

lottie-react の v3 は、2026年8月に出ました。v2 の書き方(import Lottie from 'lottie-react' や animationData)のままでは、v3 では型エラーになります。

補足

検証環境は Ubuntu 24.04、Node.js 22.23.4 です。lottie-react 3.1.2(lottie-web 5.13.0)、react / react-dom 19.3.1、@types/react 19.3.0、typescript 7.0.2、esbuild 0.28.2 を使い、.tsx を tsc で型チェックしてから esbuild でビルドしました。ブラウザは Playwright 1.63.0 で動かした Chromium 153(ヘッドレス)だけです。アニメーションの JSON は、玉が弾むだけのものを、私が Python のスクリプトで一から作りました。After Effects から書き出したファイル、エクスプレッションを使うファイル、.lottie 形式のファイル、canvas・html のレンダラー、Next.js などでのサーバーサイドレンダリング、Firefox・Safari・実機のスマートフォンは試していません。

lottie-react を入れる

sh
npm install lottie-react

公式ドキュメントのインストールの説明のとおり、描画のエンジンである lottie-web は lottie-react といっしょに入ります。検証環境でも、lottie-react 3.1.2 を入れると lottie-web 5.13.0 が入りました。react と react-dom は 18.2 以上が必要です。

v2 の書き方は v3 では型エラーになる

v3 では、既定のエクスポート(default export)がなくなり、名前付きの Lottie を読み込みます。JSON を渡す props も、animationData から src に変わりました。v2 の書き方のままのコードを tsc で型チェックすると、次のエラーが出ました(パスは node_modules から後ろだけを残しています)。

text
v2check/OldStyle.tsx(1,8): error TS2613: Module '"node_modules/lottie-react/build/index"' has no default export. Did you mean to use 'import { Lottie } from "node_modules/lottie-react/build/index"' instead?

import { Lottie } from 'lottie-react' に直しても、animationData を渡している行で「Property 'animationData' does not exist」というエラーが出ます。

もう1つの違いは既定値です。v2 からの移行ガイドによると、v2 の autoplay と loop は既定で true でしたが、v3 では false です。v2 と同じように自動で再生して繰り返すには、両方を付けます。

JSON を import して表示する

アプリといっしょに配る JSON は、import して src に渡します。TypeScript で JSON を import するために、検証では tsconfig.json に "resolveJsonModule": true を入れました。

tsx
// BouncingBall.tsx
import { Lottie } from 'lottie-react';
import bounce from './bounce.json';

export function BouncingBall() {
  return <Lottie src={bounce} autoplay loop style={{ width: 200, height: 200 }} />;
}

bounce.json は、200×200、30fps、60フレーム(2秒)のアニメーションです。表示すると、<Lottie> が出力する <div> の中に 200×200px の <svg> ができ、玉が上下に弾みました。玉の縦位置を時刻ごとに読むと、0秒で一番上、1秒で一番下、2秒でまた一番上に戻り、3秒でもう一度一番下に来ていました。loop で2秒ごとに繰り返しています。

autoplay と loop を外した <Lottie src={bounce} style={{ width: 200, height: 200 }} /> では、800ミリ秒たっても最初のフレームのまま動きませんでした。

大きさは要素で決める

<Lottie> には幅や高さの props がありません。公式ドキュメントの表示の説明によると、アニメーションは <Lottie> が出力する要素いっぱいに広がるので、style や className で要素の大きさを決めます。

大きさを指定しないと、svg レンダラー(既定のレンダラー)では、画像のように幅いっぱいに広がり、高さはアニメーションの縦横比で決まります。幅300pxの <div> の中に style なしで置いたところ、<svg> は 300×300px になりました。一方、高さが0になる要素では何も見えません。style={{ height: 0 }} を付けると、<svg> も 300×0px になりました。

URL から読み込む

src に文字列を渡すと、その URL から JSON を取得します。パスでも、https:// から始まる URL でも渡せます。

取得に失敗すると、そのままでは何も表示されません。そこで <LottieError> を置きます。<Lottie> に子要素を渡すと、<Lottie> は子要素を囲む外側の要素になるので、アニメーションを描く場所を <LottieDisplay> で示します。

tsx
// RemoteBall.tsx
import { Lottie, LottieDisplay, LottieError, type LottieHandle } from 'lottie-react';
import { useRef } from 'react';

type Props = { url: string };

export function RemoteBall({ url }: Props) {
  const handle = useRef<LottieHandle>(null);
  return (
    <Lottie src={url} autoplay loop lottieRef={handle} style={{ width: 200, height: 200 }}>
      <LottieDisplay />
      <LottieError>
        <p>アニメーションを読み込めませんでした。</p>
        <button type="button" onClick={() => handle.current?.reload()}>
          もう一度読み込む
        </button>
      </LottieError>
    </Lottie>
  );
}

<RemoteBall url="/bounce.json" /> では、/bounce.json を GET で取得して、import したときと同じように玉が弾みました。

存在しない /missing.json を渡すと、サーバーは 404 を返し、<svg> は作られず、<LottieError> の中の文と「もう一度読み込む」ボタンが出ました。ボタンを押すと、もう一度 /missing.json を取得しにいきました(結果は同じく 404 です)。

<LottieError> に子要素を渡さないと、「The animation could not be loaded.」という英語の文が出ます。日本語のページでは、上のように自分の文を入れます。読み込み中とエラーの説明によると、読み込み中に出す <LottieLoading> もあり、既定では400ミリ秒たってから出ます。

lottieRef に渡した ref からは、reload() のほか、play()、pause()、stop()、seek() などのコマンドを呼べます。ref と handle の説明によると、ref からは状態などの値は読めません。値を画面に出したいときは、次の useLottie を使う方法があります。

ボタンで再生と一時停止をする

useLottie は、状態などの値とコマンドをまとめて返す hook です。返ってきた値を <LottieDisplay lottie={...}> に渡すと、そこにアニメーションが描かれます。

tsx
// BallPlayer.tsx
import { LottieDisplay, useLottie } from 'lottie-react';
import { useRef } from 'react';
import bounce from './bounce.json';

export function BallPlayer() {
  const frameRef = useRef<HTMLSpanElement>(null);
  const lottie = useLottie({
    src: bounce,
    loop: true,
    subscriptions: {
      frame: ({ currentFrame }) => {
        if (frameRef.current) {
          frameRef.current.textContent = String(Math.round(currentFrame));
        }
      },
    },
  });

  return (
    <div>
      <LottieDisplay lottie={lottie} style={{ width: 200, height: 200 }} />
      <p>
        状態:{lottie.state}/フレーム:<span ref={frameRef}>0</span>/全{lottie.playableFrames}フレーム
      </p>
      <button type="button" onClick={() => lottie.play()}>再生</button>
      <button type="button" onClick={() => lottie.pause()}>一時停止</button>
      <button type="button" onClick={() => lottie.stop()}>最初に戻す</button>
    </div>
  );
}

lottie.state(playing や paused など)と lottie.playableFrames(フレームの数)は React の state なので、変わると再レンダリングされます。useLottie の戻り値は、いま何フレーム目かを値としては持っていません。いまのフレームは、subscriptions の frame で受け取ります。useLottie が返す値の説明では、フレームは1秒に60回変わるので、React の state に入れずに要素へ直接書くように勧めています。上のコードでも、span の textContent を書き換えています。

autoplay を付けていないので、表示した直後は「状態:stopped/フレーム:0/全60フレーム」で止まっています。「再生」を押して500ミリ秒後には、playing で15フレーム目まで進みました。「一時停止」を押すと paused になり、500ミリ秒待ってもフレームは16のままでした。「最初に戻す」を押すと stopped になり、フレームは0、玉も一番上に戻りました。

弾む玉のアニメーションの下に状態とフレームの表示と3つのボタンがある。再生、一時停止、最初に戻すを順に押すと、状態の表示が playing、paused、stopped に変わる
再生、一時停止、最初に戻すを順に押したときの動き

動きを減らす設定では自動で再生しない

OS などで動きを減らす設定(prefers-reduced-motion: reduce)をしていると、autoplay を付けていても再生は始まりません。表示の説明にそう書かれており、Playwright で Chromium の reduced motion を reduce にして BouncingBall を表示したところ、800ミリ秒たっても最初のフレームのまま動きませんでした。

この設定でも、play() を呼べば再生できます。上の BallPlayer で「再生」を押すと、設定していないときと同じように playing になり、500ミリ秒後には14フレーム目まで進んでいました。

同じ説明によると、自動で動き出して5秒より長く動くものは、一時停止できるようにすることがアクセシビリティのガイドライン(WCAG 2.2.2)で求められています。一時停止の手段は、自分で置いたボタンでもよいとされています。

軽いエンジンを使ってバンドルを小さくする

レンダラーと小さなビルドの説明によると、lottie-react には、lottie-web の小さなエンジンを使うコンポーネントが2つあります。

コンポーネント(hook) 使えるレンダラー エクスプレッションとレイヤーのエフェクト
Lottie(useLottie) svg、canvas、html 使える
LottieSvg(useLottieSvg) svg 使える
LottieLight(useLottieLight) svg 使えない

svg 以外のレンダラーを選べないことを除けば、使い方は Lottie と同じです。import { LottieLight } from 'lottie-react' に替えるだけで使えます。

それぞれ1つだけを使うページを esbuild でバンドルし、minify と gzip をしたあとの大きさを測りました。React だけのページと比べて増えたのは、Lottie が83.0KB、LottieSvg が69.2KB、LottieLight が54.1KB でした(1KB=1,000バイトで計算)。ドキュメントに書かれているエンジン込みの大きさ(約81KB、約67KB、約51KB)と近い値です。

Lottie、LottieSvg、LottieLight で増えたバンドルの大きさを比べた横棒グラフ。83.0KB、69.2KB、54.1KB
React だけのページと比べて増えた大きさ(minify と gzip 後)

LottieLight は、エクスプレッションを評価せず、レイヤーのエフェクトもかけません。ドキュメントによると、エクスプレッションやエフェクトで動かしている値は、それらを使わないときの値のまま描かれます。エクスプレッションを使っていない今回の JSON は、LottieLight に替えても同じように弾みました。

また、LottieLight のエンジンは eval を含みません。ドキュメントによると、厳しい Content Security Policy を設定したページでは、この違いが関係してきます。

​Posted by 86