T-CREATOR

Motion(旧 Framer Motion)useAnimate/useMotionValueEvent 速習チートシート

Motion(旧 Framer Motion)useAnimate/useMotionValueEvent 速習チートシート

Motion(旧 Framer Motion)で高度なアニメーションを実装する際、useAnimateuseMotionValueEvent は非常に強力なツールです。 この記事では、これら 2 つのフックに焦点を絞り、実践的な使い方を詳しく解説していきます。

複雑なアニメーションシーケンスの制御や、アニメーション値の変化に応じた処理を実装したい方にとって、必須のテクニックとなるでしょう。

速習早見表

useAnimate クイックリファレンス

#用途基本構文使用例
1基本的なアニメーションawait animate(selector, values, options)await animate('.box', { x: 100 }, { duration: 0.5 })
2連続アニメーションawait で順番に実行await animate('.item-1', {...})await animate('.item-2', {...})
3並列アニメーションawait なしで実行animate('.box-1', {...})animate('.box-2', {...}) を同時実行
4時間差アニメーションdelay オプションを使用animate(item, {...}, { delay: index * 0.1 })
5キーフレーム配列で値を指定animate('.card', { x: [0, -10, 10, 0] })

useMotionValueEvent クイックリファレンス

#用途基本構文使用例
1スクロール進捗監視useMotionValueEvent(scrollYProgress, 'change', callback)スクロール位置に応じた処理実行
2スクロール量監視useMotionValueEvent(scrollY, 'change', callback)ピクセル単位でのスクロール量取得
3ドラッグ座標監視useMotionValueEvent(x​/​y, 'change', callback)ドラッグ中の座標をリアルタイム取得
4カスタム値監視useMotionValueEvent(customValue, 'change', callback)任意の MotionValue の変化を監視

使い分けガイド

#シチュエーション推奨フック実装パターン
1ボタンクリックで順番にアニメーションuseAnimateawait を使った連続実行
2ページ読み込み時の演出useAnimateuseEffect 内で animate を実行
3スクロール連動の背景色変更useMotionValueEventscrollY を監視して状態更新
4ドラッグ操作の座標表示useMotionValueEventx, y の MotionValue を監視
5フォーム送信後のアニメーション演出useAnimate成功/失敗で条件分岐
6スクロール進捗バーuseMotionValueEventscrollYProgress を監視
7複数要素の時間差表示useAnimatedelay オプションで stagger 実装
8スクロールトリガーアニメーション両方組み合わせuseMotionValueEvent で監視 + useAnimate で実行

よく使うオプション一覧

useAnimate のオプション

#オプション説明デフォルト
1durationnumberアニメーション時間(秒)0.3
2delaynumber遅延時間(秒)0
3easestring | functionイージング関数"easeInOut"
4repeatnumber繰り返し回数0
5repeatType"loop" | "reverse" | "mirror"繰り返しの種類"loop"

よく使うイージング

#イージング名用途視覚的効果
1"linear"一定速度機械的な動き
2"easeIn"ゆっくり開始加速感
3"easeOut"ゆっくり終了減速感(推奨)
4"easeInOut"両端がゆっくり自然な動き(推奨)
5[0.4, 0, 0.2, 1]カスタムベジェ曲線細かい調整が可能

背景

Motion ライブラリの進化

Motion(旧 Framer Motion)は、React アプリケーションにおけるアニメーション実装の標準的なライブラリとして広く使われています。 基本的な motion コンポーネントだけでも多くのことが実現できますが、より複雑なアニメーション制御が必要になるケースも増えてきました。

従来の motion コンポーネントは宣言的な API を提供していますが、命令的な制御やリアルタイムの値監視が必要な場合には限界があります。 そこで登場したのが useAnimateuseMotionValueEvent です。

2 つのフックの役割

これらのフックは、それぞれ異なる目的で使用されます。

mermaidflowchart TB
  motion["motion コンポーネント<br/>(宣言的 API)"]
  useAnimate["useAnimate<br/>(命令的アニメーション制御)"]
  useMotionValueEvent["useMotionValueEvent<br/>(値変化の監視)"]

  motion -->|より細かい制御が必要| useAnimate
  motion -->|値の変化を検知したい| useMotionValueEvent

  useAnimate -->|シーケンス制御| seq["複雑なアニメーション<br/>シーケンス"]
  useAnimate -->|タイミング制御| timing["細かいタイミング調整"]

  useMotionValueEvent -->|イベント処理| event["スクロール連動処理"]
  useMotionValueEvent -->|状態同期| sync["他の状態との同期"]

図のポイント:

  • useAnimate は命令的にアニメーションを制御する際に使用
  • useMotionValueEvent は motion の値変化を監視し、リアクティブな処理を実行

課題

宣言的 API の限界

通常の motion コンポーネントでは、以下のようなケースで実装が困難になります。

#課題具体例
1複数要素の連続アニメーションボタンクリック後に順番に複数の要素をアニメーション
2条件分岐を含むアニメーションユーザーの操作に応じて異なるアニメーションパスを選択
3アニメーション値に基づく処理スクロール位置に応じて別コンポーネントの状態を変更
4動的なタイミング調整API レスポンスの内容によってアニメーション速度を変更

従来の解決策とその問題点

これまでは以下のような方法で対応していました。

typescript// ❌ 従来の方法:useEffect と animate の組み合わせ
import { motion, useAnimation } from 'framer-motion';
import { useEffect } from 'react';
typescriptfunction OldApproach() {
  const controls = useAnimation();

  useEffect(() => {
    // 複雑な制御が必要な場合、コードが煩雑に
    controls
      .start({ x: 100 })
      .then(() => controls.start({ y: 100 }))
      .then(() => controls.start({ opacity: 0 }));
  }, [controls]);

  return <motion.div animate={controls} />;
}

この方法には以下の問題がありました。

  • コンポーネントの ref を取得する必要がある
  • 複数要素の制御が煩雑
  • TypeScript との相性が悪い
  • コードの可読性が低い

解決策

useAnimate による命令的制御

useAnimate フックは、命令的な方法でアニメーションを制御するための新しい API です。 従来の useAnimation よりも直感的で、TypeScript のサポートも充実しています。

基本構文

typescriptimport { useAnimate } from 'motion/react';
typescriptfunction BasicExample() {
  // scope: アニメーション対象の要素への参照
  // animate: アニメーション実行関数
  const [scope, animate] = useAnimate();

  return <div ref={scope}>アニメーション対象</div>;
}

useAnimate は 2 つの値を返します。 scope は対象要素への参照、animate はアニメーションを実行する関数です。

アニメーション実行の基本形

typescript// 基本的なアニメーション実行
async function runAnimation() {
  // 第 1 引数: セレクタ(CSS セレクタまたは要素)
  // 第 2 引数: アニメーションの値
  // 第 3 引数: オプション(duration, delay など)
  await animate('.box', { x: 100 }, { duration: 0.5 });
}

useMotionValueEvent によるイベント処理

useMotionValueEvent は、motion の値(MotionValue)の変化を監視し、その変化に応じた処理を実行するためのフックです。 スクロール連動やドラッグ操作など、リアルタイムな値の変化に対応できます。

基本構文

typescriptimport {
  useMotionValueEvent,
  useScroll,
} from 'motion/react';
import { useState } from 'react';
typescriptfunction ScrollProgress() {
  const [progress, setProgress] = useState(0);
  const { scrollYProgress } = useScroll();

  // scrollYProgress の変化を監視
  useMotionValueEvent(
    scrollYProgress,
    'change',
    (latest) => {
      // latest: 最新の値(0 ~ 1)
      setProgress(Math.round(latest * 100));
    }
  );

  return <div>スクロール進捗: {progress}%</div>;
}

第 1 引数に監視対象の MotionValue、第 2 引数にイベント名(通常は 'change')、第 3 引数にコールバック関数を指定します。

2 つのフックの使い分け

以下の表を参考に、適切なフックを選択してください。

#状況使用するフック理由
1ボタンクリックで複数要素を順番にアニメーションuseAnimate命令的なシーケンス制御が必要
2スクロール位置に応じて背景色を変更useMotionValueEvent値の変化を継続的に監視
3フォーム送信後のアニメーション演出useAnimateイベントドリブンな制御
4ドラッグ中の座標を表示useMotionValueEventリアルタイムな値の取得

具体例

useAnimate の実践例

例 1: 複数要素の連続アニメーション

ボタンをクリックすると、3 つの要素が順番にフェードインするアニメーションです。

typescriptimport { useAnimate } from 'motion/react';
typescriptfunction SequenceAnimation() {
  const [scope, animate] = useAnimate();

  const handleClick = async () => {
    // 初期状態にリセット
    await animate(
      '.item',
      { opacity: 0, y: 20 },
      { duration: 0 }
    );

    // 順番にアニメーション(await で待機)
    await animate(
      '.item-1',
      { opacity: 1, y: 0 },
      { duration: 0.3 }
    );
    await animate(
      '.item-2',
      { opacity: 1, y: 0 },
      { duration: 0.3 }
    );
    await animate(
      '.item-3',
      { opacity: 1, y: 0 },
      { duration: 0.3 }
    );
  };

  return (
    <div ref={scope}>
      <button onClick={handleClick}>
        アニメーション開始
      </button>
      <div className='item item-1'>要素 1</div>
      <div className='item item-2'>要素 2</div>
      <div className='item item-3'>要素 3</div>
    </div>
  );
}

await を使用することで、各アニメーションの完了を待ってから次のアニメーションを開始できます。 この方法により、複雑なシーケンスも直感的に記述できるのです。

例 2: 並列アニメーションの実行

複数の要素を同時にアニメーションさせる場合は、await を使用せずに実行します。

typescriptfunction ParallelAnimation() {
  const [scope, animate] = useAnimate();

  const handleClick = () => {
    // 並列実行(await なし)
    animate(
      '.box-1',
      { x: 100, rotate: 90 },
      { duration: 0.5 }
    );
    animate(
      '.box-2',
      { x: -100, rotate: -90 },
      { duration: 0.5 }
    );
    animate(
      '.box-3',
      { y: 100, scale: 1.5 },
      { duration: 0.5 }
    );
  };

  return (
    <div ref={scope}>
      <button onClick={handleClick}>
        並列アニメーション
      </button>
      <div className='box-1'>Box 1</div>
      <div className='box-2'>Box 2</div>
      <div className='box-3'>Box 3</div>
    </div>
  );
}

例 3: 条件分岐を含むアニメーション

ユーザーの選択に応じて異なるアニメーションを実行する例です。

typescriptfunction ConditionalAnimation() {
  const [scope, animate] = useAnimate();

  const handleSuccess = async () => {
    // 成功時: 緑色にフェードイン後、上にスライドアウト
    await animate(
      '.card',
      { backgroundColor: '#10b981' },
      { duration: 0.3 }
    );
    await animate(
      '.card',
      { y: -100, opacity: 0 },
      { duration: 0.5 }
    );
  };

  const handleError = async () => {
    // エラー時: 赤色に変更後、左右に揺れる
    await animate(
      '.card',
      { backgroundColor: '#ef4444' },
      { duration: 0.3 }
    );
    await animate(
      '.card',
      { x: [0, -10, 10, -10, 10, 0] },
      { duration: 0.5 }
    );
  };

  return (
    <div ref={scope}>
      <div className='card'>カード要素</div>
      <button onClick={handleSuccess}>成功</button>
      <button onClick={handleError}>エラー</button>
    </div>
  );
}

配列を使用することで、キーフレームアニメーションも簡単に実装できます。

例 4: stagger(時間差)アニメーション

複数要素に時間差をつけてアニメーションする際は、オプションで delay を指定します。

typescriptfunction StaggerAnimation() {
  const [scope, animate] = useAnimate();

  const handleClick = async () => {
    const items = [
      '.item-1',
      '.item-2',
      '.item-3',
      '.item-4',
    ];

    // 各要素に 0.1 秒ずつ遅延を追加
    items.forEach((item, index) => {
      animate(
        item,
        { opacity: 1, x: 0 },
        { duration: 0.5, delay: index * 0.1 }
      );
    });
  };

  return (
    <div ref={scope}>
      <button onClick={handleClick}>スタガー開始</button>
      <div
        className='item item-1'
        style={{
          opacity: 0,
          transform: 'translateX(-20px)',
        }}
      >
        アイテム 1
      </div>
      <div
        className='item item-2'
        style={{
          opacity: 0,
          transform: 'translateX(-20px)',
        }}
      >
        アイテム 2
      </div>
      <div
        className='item item-3'
        style={{
          opacity: 0,
          transform: 'translateX(-20px)',
        }}
      >
        アイテム 3
      </div>
      <div
        className='item item-4'
        style={{
          opacity: 0,
          transform: 'translateX(-20px)',
        }}
      >
        アイテム 4
      </div>
    </div>
  );
}

useMotionValueEvent の実践例

例 5: スクロール進捗の可視化

ページのスクロール進捗をプログレスバーで表示する例です。

typescriptimport {
  useMotionValueEvent,
  useScroll,
  motion,
} from 'motion/react';
import { useState } from 'react';
typescriptfunction ScrollProgressBar() {
  const [progress, setProgress] = useState(0);
  const { scrollYProgress } = useScroll();

  // scrollYProgress は 0 ~ 1 の値
  useMotionValueEvent(
    scrollYProgress,
    'change',
    (latest) => {
      setProgress(latest);
    }
  );

  return (
    <motion.div
      style={{
        position: 'fixed',
        top: 0,
        left: 0,
        right: 0,
        height: '5px',
        backgroundColor: '#3b82f6',
        transformOrigin: '0%',
        scaleX: progress,
      }}
    />
  );
}

scrollYProgress は画面全体のスクロール進捗を 0 から 1 の値で提供します。 この値をリアルタイムで監視し、プログレスバーの幅に反映させているのです。

例 6: スクロール位置に応じた背景色変更

スクロール位置によって背景色をスムーズに変更する例です。

typescriptfunction ScrollColorChange() {
  const [bgColor, setBgColor] = useState('#ffffff');
  const { scrollY } = useScroll();

  useMotionValueEvent(scrollY, 'change', (latest) => {
    // スクロール量に応じて色を変更
    if (latest < 300) {
      setBgColor('#ffffff'); //
    } else if (latest < 600) {
      setBgColor('#dbeafe'); // 薄い青
    } else if (latest < 900) {
      setBgColor('#93c5fd'); //
    } else {
      setBgColor('#3b82f6'); // 濃い青
    }
  });

  return (
    <div
      style={{
        backgroundColor: bgColor,
        transition: 'background-color 0.5s',
      }}
    >
      {/* ページコンテンツ */}
    </div>
  );
}

例 7: ドラッグ座標のリアルタイム表示

ドラッグ可能な要素の座標をリアルタイムで表示する例です。

typescriptfunction DragCoordinates() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  return (
    <>
      <motion.div
        drag
        onUpdate={(latest) => {
          // ドラッグ中の座標を取得
          if (
            latest.x !== undefined &&
            latest.y !== undefined
          ) {
            setPosition({
              x: Math.round(latest.x),
              y: Math.round(latest.y),
            });
          }
        }}
        style={{
          width: 100,
          height: 100,
          backgroundColor: '#3b82f6',
          borderRadius: 8,
          cursor: 'grab',
        }}
      />
      <div>
        X座標: {position.x}px, Y座標: {position.y}px
      </div>
    </>
  );
}

この例では onUpdate プロパティを使用していますが、useMotionValue と組み合わせることでより柔軟な制御が可能です。

typescriptimport { motion, useMotionValue } from 'motion/react';
typescriptfunction DragWithMotionValue() {
  const [displayX, setDisplayX] = useState(0);
  const [displayY, setDisplayY] = useState(0);

  const x = useMotionValue(0);
  const y = useMotionValue(0);

  // X 座標の変化を監視
  useMotionValueEvent(x, 'change', (latest) => {
    setDisplayX(Math.round(latest));
  });

  // Y 座標の変化を監視
  useMotionValueEvent(y, 'change', (latest) => {
    setDisplayY(Math.round(latest));
  });

  return (
    <>
      <motion.div
        drag
        style={{
          x,
          y,
          width: 100,
          height: 100,
          backgroundColor: '#3b82f6',
        }}
      />
      <div>
        X: {displayX}px, Y: {displayY}px
      </div>
    </>
  );
}

useMotionValue を使用することで、アニメーション値を直接監視できます。

例 8: スクロール連動型の要素表示制御

スクロール位置に応じて要素の表示・非表示を切り替える例です。

typescriptfunction ScrollReveal() {
  const [isVisible, setIsVisible] = useState(false);
  const { scrollY } = useScroll();

  useMotionValueEvent(scrollY, 'change', (latest) => {
    // 300px 以上スクロールしたら表示
    setIsVisible(latest > 300);
  });

  return (
    <motion.div
      animate={{
        opacity: isVisible ? 1 : 0,
        y: isVisible ? 0 : 20,
      }}
      transition={{ duration: 0.5 }}
      style={{
        position: 'fixed',
        bottom: 20,
        right: 20,
        padding: '10px 20px',
        backgroundColor: '#3b82f6',
        color: 'white',
        borderRadius: 8,
      }}
    >
      トップに戻る
    </motion.div>
  );
}

組み合わせ例: 高度なインタラクション

useAnimateuseMotionValueEvent を組み合わせることで、より高度なインタラクションが実現できます。

例 9: スクロール位置に応じた複雑なアニメーション

typescriptfunction ScrollTriggeredSequence() {
  const [scope, animate] = useAnimate();
  const { scrollYProgress } = useScroll();
  const [hasAnimated, setHasAnimated] = useState(false);

  useMotionValueEvent(
    scrollYProgress,
    'change',
    async (latest) => {
      // 50% スクロールした時点で一度だけアニメーション実行
      if (latest > 0.5 && !hasAnimated) {
        setHasAnimated(true);

        // 複雑なアニメーションシーケンス
        await animate(
          '.hero-title',
          { opacity: 1, y: 0 },
          { duration: 0.5 }
        );
        await animate(
          '.hero-subtitle',
          { opacity: 1, y: 0 },
          { duration: 0.5 }
        );

        // 複数のカードを並列アニメーション
        animate(
          '.card-1',
          { opacity: 1, scale: 1 },
          { duration: 0.3, delay: 0 }
        );
        animate(
          '.card-2',
          { opacity: 1, scale: 1 },
          { duration: 0.3, delay: 0.1 }
        );
        animate(
          '.card-3',
          { opacity: 1, scale: 1 },
          { duration: 0.3, delay: 0.2 }
        );
      }
    }
  );

  return (
    <div ref={scope}>
      <h1
        className='hero-title'
        style={{
          opacity: 0,
          transform: 'translateY(20px)',
        }}
      >
        タイトル
      </h1>
      <p
        className='hero-subtitle'
        style={{
          opacity: 0,
          transform: 'translateY(20px)',
        }}
      >
        サブタイトル
      </p>
      <div
        className='card-1'
        style={{ opacity: 0, transform: 'scale(0.8)' }}
      >
        カード 1
      </div>
      <div
        className='card-2'
        style={{ opacity: 0, transform: 'scale(0.8)' }}
      >
        カード 2
      </div>
      <div
        className='card-3'
        style={{ opacity: 0, transform: 'scale(0.8)' }}
      >
        カード 3
      </div>
    </div>
  );
}

この例では、スクロール監視とアニメーション制御を組み合わせることで、ユーザー体験を大きく向上させています。

パフォーマンス最適化のヒント

アニメーションのパフォーマンスを最適化するためのポイントを紹介します。

#項目推奨方法理由
1変換プロパティx, y, scale, rotate を使用GPU アクセラレーションが有効
2避けるべきプロパティwidth, height, top, leftレイアウト再計算が発生
3イベント頻度必要最小限の監視に留める過度な再レンダリングを防ぐ
4メモ化コールバック関数を useCallback でメモ化不要な再生成を防ぐ
typescriptimport { useCallback } from 'react';
typescriptfunction OptimizedAnimation() {
  const [scope, animate] = useAnimate();
  const { scrollY } = useScroll();

  // コールバックをメモ化
  const handleScrollChange = useCallback(
    (latest: number) => {
      // 100px 単位でのみ処理(頻度を削減)
      const threshold = Math.floor(latest / 100) * 100;

      if (threshold > 500) {
        // 処理を実行
      }
    },
    []
  );

  useMotionValueEvent(
    scrollY,
    'change',
    handleScrollChange
  );

  return <div ref={scope}>{/* コンテンツ */}</div>;
}

まとめ

この記事では、Motion(旧 Framer Motion)の useAnimateuseMotionValueEvent について詳しく解説しました。

useAnimate のポイント:

  • 命令的にアニメーションを制御できる強力なフック
  • async​/​await で直感的なシーケンス制御が可能
  • CSS セレクタで複数要素を効率的に制御
  • TypeScript との相性が良く、型安全な実装が実現

useMotionValueEvent のポイント:

  • MotionValue の変化をリアルタイムで監視
  • スクロール連動やドラッグ操作に最適
  • React の状態と Motion の値を連携
  • パフォーマンスを考慮した実装が重要

これら 2 つのフックを適切に使い分けることで、ユーザー体験を大きく向上させる魅力的なアニメーションが実装できるでしょう。 シンプルな宣言的 API と組み合わせることで、Motion の真の力を引き出せます。

複雑なアニメーションも、これらのツールを使えば驚くほど簡単に実装できますので、ぜひ実際のプロジェクトで試してみてください。

関連リンク