T-CREATOR

Motion(旧 Framer Motion)基本 API 徹底解説:motion 要素・initial/animate/exit の正しい使い方

Motion(旧 Framer Motion)基本 API 徹底解説:motion 要素・initial/animate/exit の正しい使い方

React でアニメーションを実装する際、Motion は最も強力で直感的なライブラリの一つです。従来の複雑なアニメーション実装から解放され、わずか数行のコードで美しいアニメーションが作成できる魅力があります。

この記事では、Motion の基本となる motion 要素と、initialanimateexit の 3 つの核となるプロパティの正しい使い方を、実践的なコード例とともに詳しく解説していきます。初心者の方でも段階的に理解できるよう、基礎から応用まで丁寧に説明いたします。

背景

Motion とは何か

Motion は React 専用のアニメーションライブラリで、宣言的な記述でスムーズなアニメーションを実現できます。2018 年に Framer Motion として登場し、現在は Motion として継続開発されています。

最大の特徴は、複雑な JavaScript のアニメーション処理を React のコンポーネント記法に自然に統合できる点です。従来のアニメーション実装で必要だった手動での DOM 操作や状態管理が不要になり、React の思想に沿った形でアニメーションが記述できるのです。

React アニメーションライブラリの現状

React エコシステムには様々なアニメーションライブラリが存在しています。以下に主要なライブラリの特徴をまとめます。

ライブラリ名特徴学習コストの高さパフォーマンス
Motion宣言的、豊富な機能
React SpringHook ベース、物理演算
React Transition Group軽量、基本的な遷移
Lottie ReactAfter Effects 連携

Motion は他のライブラリと比較して、記述のシンプルさとパフォーマンスのバランスが優秀です。特に、CSS のアニメーションプロパティに近い感覚で書けるため、フロントエンド開発者にとって直感的に扱えるのが大きなメリットです。

Motion の位置づけと特徴

現代の Web 開発において、アニメーションは単なる装飾ではなく、ユーザーエクスペリエンスを向上させる重要な要素となりました。Motion はこの流れに対応し、以下の特徴を持っています。

  • 宣言的記述: アニメーションの最終状態を記述するだけで、中間フレームは自動生成
  • 高パフォーマンス: GPU アクセラレーションを活用したスムーズな動作
  • TypeScript 完全対応: 型安全性を保ちながら開発可能
  • 豊富な API: シンプルなフェードインから複雑なジェスチャーアニメーションまで対応

以下の図は、Motion がどのように React コンポーネントと統合されるかを示しています。

mermaidflowchart TB
  react[React コンポーネント] --> motion[motion 要素]
  motion --> props[アニメーションプロパティ]
  props --> initial[初期状態定義]
  props --> animate[アニメーション状態]
  props --> exit[終了状態]
  
  initial --> render[DOM レンダリング]
  animate --> render
  exit --> render
  render --> gpu[GPU アクセラレーション]
  gpu --> smooth[スムーズアニメーション]

この構成により、React の仮想 DOM と Motion のアニメーションエンジンがシームレスに連携し、最適化されたアニメーションが実現されます。

課題

従来のCSS アニメーションの限界

CSS だけでアニメーションを実装する場合、以下のような課題があります。

状態管理の複雑さ

CSS のキーフレームアニメーションは、アニメーションの開始・終了タイミングを JavaScript で制御する必要があります。これにより、React の状態管理とアニメーションの同期が困難になることがありました。

css/* 複雑なクラス管理が必要 */
.fade-enter {
  opacity: 0;
}

.fade-enter-active {
  opacity: 1;
  transition: opacity 300ms;
}

.fade-exit {
  opacity: 1;
}

.fade-exit-active {
  opacity: 0;
  transition: opacity 300ms;
}

動的なアニメーションの実装困難さ

プロパティの値を動的に変更したり、条件に応じてアニメーションを切り替えたりする場合、CSS だけでは限界があります。JavaScript での細かい制御が必要になり、コードが複雑になってしまうのです。

React でのアニメーション実装の難しさ

React の仮想 DOM の仕組みは、直接的な DOM 操作を前提とした従来のアニメーションライブラリとは相性が良くありませんでした。

ライフサイクルとの同期問題

React コンポーネントのマウント・アンマウントとアニメーションのタイミングを同期させるのが複雑でした。特に、コンポーネントが削除される前にアニメーションを完了させる処理は、多くの開発者を悩ませてきました。

再レンダリングによる処理中断

React の再レンダリングによって、実行中のアニメーションが中断されたり、期待した動作にならなかったりする問題がありました。

以下の図は、従来のアニメーション実装における課題を示しています。

mermaidsequenceDiagram
  participant React as React コンポーネント
  participant CSS as CSS アニメーション
  participant JS as JavaScript 制御
  participant DOM as DOM 要素

  React->>DOM: 初期レンダリング
  React->>JS: アニメーション開始指示
  JS->>CSS: クラス付与
  CSS->>DOM: アニメーション実行
  
  Note over React,DOM: 再レンダリング発生
  React->>DOM: 強制更新
  CSS-->>DOM: アニメーション中断
  
  Note over React,DOM: 同期がとれない状態

このシーケンス図から分かるように、React の更新サイクルと CSS アニメーションの実行タイミングにずれが生じることで、アニメーションが中途半端な状態で終わってしまう問題がありました。

パフォーマンスとUX の両立

美しいアニメーションを実現したいものの、パフォーマンスを犠牲にするわけにはいきません。特に以下の点が課題でした。

60fps を維持する難しさ

スムーズなアニメーションには 60fps(16.67ms ごとの更新)が必要ですが、JavaScript の処理負荷によってフレームレートが低下しやすい問題がありました。

メモリリークのリスク

アニメーション処理が適切に終了せず、メモリリークを起こすリスクがありました。特に、タイマーやリスナーの適切な解放処理を忘れがちでした。

解決策

Motion の基本コンセプト

Motion は上記の課題を解決するために、以下のコンセプトを採用しています。

宣言的アニメーション記述

従来の命令的な記述(「これをして、次にこれをして...」)ではなく、宣言的な記述(「最終的にこの状態になる」)でアニメーションを定義できます。これにより、コードの可読性が向上し、メンテナンスが容易になります。

React ライフサイクルとの完全統合

Motion は React のコンポーネントライフサイクルと完全に統合されており、マウント・アンマウント時のアニメーションを自動的に管理してくれます。

motion 要素の仕組み

Motion の中核となるのが motion 要素です。通常の HTML 要素を motion.divmotion.span などの motion 要素に置き換えることで、アニメーション機能が利用できます。

motion 要素は以下のような仕組みで動作します:

mermaidflowchart LR
  component[React コンポーネント] --> motion_element[motion.div]
  motion_element --> props[アニメーションプロパティ]
  
  subgraph "Motion エンジン"
    props --> parser[プロパティ解析]
    parser --> timeline[タイムライン生成]
    timeline --> renderer[レンダラー]
  end
  
  renderer --> dom[DOM 更新]
  dom --> gpu[GPU アクセラレーション]
  gpu --> animation[スムーズアニメーション]

この仕組みにより、開発者は複雑なアニメーション処理を意識することなく、プロパティの設定だけでアニメーションを実現できます。

宣言的アニメーションのメリット

宣言的アニメーション記述により、以下のメリットが得られます:

コードの簡潔性

従来 50 行以上必要だったアニメーション処理が、わずか数行で記述できるようになります。

デバッグのしやすさ

アニメーションの状態がプロパティとして明確に定義されるため、問題の特定が容易です。

再利用性の向上

アニメーションパターンをコンポーネント化して、他の場所で簡単に再利用できます。

具体例

motion 要素の基本使用法

まず、Motion を使用するための基本的なセットアップから始めましょう。

インストール

bashyarn add motion

基本的なインポート

typescriptimport { motion } from 'motion/react'

Motion では、通常の HTML 要素を motion プレフィックス付きの要素に置き換えることで、アニメーション機能を追加できます。

最も基本的な motion 要素

tsxfunction BasicExample() {
  return (
    <motion.div
      style={{
        width: 100,
        height: 100,
        backgroundColor: '#3b82f6'
      }}
    >
      基本的な motion 要素
    </motion.div>
  )
}

この時点では通常の div 要素と同じように動作しますが、アニメーションプロパティを追加することで動きを付けられるようになります。

initial プロパティの詳細解説

initial プロパティは、アニメーションの開始状態を定義します。コンポーネントがマウントされた瞬間の状態を指定できます。

基本的な initial の使い方

tsxfunction FadeInExample() {
  return (
    <motion.div
      initial={{ opacity: 0 }}
      style={{
        width: 200,
        height: 100,
        backgroundColor: '#10b981'
      }}
    >
      フェードインする要素
    </motion.div>
  )
}

上記のコードでは、要素は透明度 0(完全に透明)の状態で表示されます。まだアニメーションは発生しませんが、初期状態が設定されました。

複数のプロパティを同時に設定

tsxfunction MultipleInitialExample() {
  return (
    <motion.div
      initial={{
        opacity: 0,
        scale: 0.8,
        y: 20
      }}
      style={{
        width: 200,
        height: 100,
        backgroundColor: '#8b5cf6'
      }}
    >
      複数の初期状態を持つ要素
    </motion.div>
  )
}

この例では、透明度、スケール、Y 軸の位置を同時に初期状態として設定しています。要素は半透明で小さく、少し下にずれた状態で表示されます。

initial を false に設定

tsxfunction NoInitialExample() {
  return (
    <motion.div
      initial={false}
      style={{
        width: 200,
        height: 100,
        backgroundColor: '#ef4444'
      }}
    >
      初期アニメーションをスキップ
    </motion.div>
  )
}

initial={false} を指定すると、初期アニメーションがスキップされ、要素は通常の状態で即座に表示されます。

animate プロパティの活用方法

animate プロパティは、要素が到達すべき最終状態を定義します。Motion は自動的に initial から animate への遷移アニメーションを生成します。

基本的なフェードイン アニメーション

tsxfunction FadeInAnimation() {
  return (
    <motion.div
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      style={{
        width: 200,
        height: 100,
        backgroundColor: '#06b6d4'
      }}
    >
      フェードインアニメーション
    </motion.div>
  )
}

この例では、透明度 0 から 1 への滑らかな遷移が自動的に生成されます。デフォルトのアニメーション時間は 0.3 秒です。

複雑な変形アニメーション

tsxfunction ComplexAnimation() {
  return (
    <motion.div
      initial={{
        opacity: 0,
        scale: 0.5,
        rotate: -10,
        y: 50
      }}
      animate={{
        opacity: 1,
        scale: 1,
        rotate: 0,
        y: 0
      }}
      transition={{
        duration: 0.6,
        ease: "easeOut"
      }}
      style={{
        width: 150,
        height: 150,
        backgroundColor: '#f59e0b',
        borderRadius: 8
      }}
    >
      複雑な変形アニメーション
    </motion.div>
  )
}

この例では、フェードイン、スケール変更、回転、位置移動を同時に実行します。transition プロパティで、アニメーション時間とイージング関数をカスタマイズしています。

状態に応じた動的なアニメーション

tsxfunction DynamicAnimation() {
  const [isHovered, setIsHovered] = useState(false)

  return (
    <motion.button
      animate={{
        scale: isHovered ? 1.1 : 1,
        backgroundColor: isHovered ? '#3b82f6' : '#6b7280'
      }}
      onHoverStart={() => setIsHovered(true)}
      onHoverEnd={() => setIsHovered(false)}
      style={{
        padding: '12px 24px',
        border: 'none',
        borderRadius: 6,
        color: 'white',
        cursor: 'pointer'
      }}
    >
      ホバーでアニメーション
    </motion.button>
  )
}

状態が変更されるたびに、Motion は自動的に現在の状態から新しい animate 状態への遷移を生成します。

exit プロパティとアンマウント時の処理

exit プロパティは、コンポーネントが DOM から削除される際のアニメーションを定義します。ただし、exit アニメーションを実行するには AnimatePresence コンポーネントが必要です。

基本的なexit アニメーション

tsximport { motion, AnimatePresence } from 'motion/react'

function ExitExample() {
  const [isVisible, setIsVisible] = useState(true)

  return (
    <div>
      <button onClick={() => setIsVisible(!isVisible)}>
        {isVisible ? '非表示にする' : '表示する'}
      </button>
      
      <AnimatePresence>
        {isVisible && (
          <motion.div
            initial={{ opacity: 0, scale: 0.8 }}
            animate={{ opacity: 1, scale: 1 }}
            exit={{ opacity: 0, scale: 0.8 }}
            transition={{ duration: 0.3 }}
            style={{
              width: 200,
              height: 100,
              backgroundColor: '#ec4899',
              marginTop: 20
            }}
          >
            exit アニメーション
          </motion.div>
        )}
      </AnimatePresence>
    </div>
  )
}

AnimatePresence により、コンポーネントが削除される前に exit アニメーションが実行され、完了後に DOM から削除されます。

リストアイテムの削除アニメーション

tsxfunction TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'React を学ぶ' },
    { id: 2, text: 'Motion をマスターする' },
    { id: 3, text: 'アプリを公開する' }
  ])

  const removeTodo = (id: number) => {
    setTodos(todos.filter(todo => todo.id !== id))
  }

  return (
    <ul style={{ listStyle: 'none', padding: 0 }}>
      <AnimatePresence>
        {todos.map((todo) => (
          <motion.li
            key={todo.id}
            initial={{ opacity: 0, x: -20 }}
            animate={{ opacity: 1, x: 0 }}
            exit={{ opacity: 0, x: 20, backgroundColor: '#fef2f2' }}
            transition={{ duration: 0.2 }}
            style={{
              padding: 12,
              marginBottom: 8,
              backgroundColor: '#f3f4f6',
              borderRadius: 6,
              cursor: 'pointer'
            }}
            onClick={() => removeTodo(todo.id)}
          >
            {todo.text}
          </motion.li>
        ))}
      </AnimatePresence>
    </ul>
  )
}

この例では、リストアイテムがクリックされると右にスライドしながらフェードアウトし、背景色も変化して削除されることを視覚的に示します。

実践的なアニメーション例

ページ遷移アニメーション

tsxfunction PageTransition({ children }: { children: React.ReactNode }) {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      exit={{ opacity: 0, y: -20 }}
      transition={{
        duration: 0.3,
        ease: "easeInOut"
      }}
    >
      {children}
    </motion.div>
  )
}

// 使用例
function App() {
  const [currentPage, setCurrentPage] = useState('home')

  return (
    <AnimatePresence mode="wait">
      {currentPage === 'home' && (
        <PageTransition key="home">
          <HomePage />
        </PageTransition>
      )}
      {currentPage === 'about' && (
        <PageTransition key="about">
          <AboutPage />
        </PageTransition>
      )}
    </AnimatePresence>
  )
}

mode="wait" により、前のページの exit アニメーションが完了してから、次のページの enter アニメーションが開始されます。

ステージングアニメーション(順次表示)

tsxfunction StaggeredList() {
  const items = ['項目 1', '項目 2', '項目 3', '項目 4']

  return (
    <motion.ul
      initial="hidden"
      animate="visible"
      variants={{
        hidden: { opacity: 0 },
        visible: {
          opacity: 1,
          transition: {
            staggerChildren: 0.1
          }
        }
      }}
      style={{ listStyle: 'none', padding: 0 }}
    >
      {items.map((item, index) => (
        <motion.li
          key={index}
          variants={{
            hidden: { opacity: 0, x: -20 },
            visible: { opacity: 1, x: 0 }
          }}
          style={{
            padding: 12,
            marginBottom: 8,
            backgroundColor: '#dbeafe',
            borderRadius: 6
          }}
        >
          {item}
        </motion.li>
      ))}
    </motion.ul>
  )
}

staggerChildren により、子要素が順次アニメーションされ、美しいカスケード効果が生まれます。

カスタムトランジション設定

tsxfunction CustomTransition() {
  const [isExpanded, setIsExpanded] = useState(false)

  return (
    <motion.div
      animate={{
        height: isExpanded ? 200 : 60,
        backgroundColor: isExpanded ? '#dcfce7' : '#f3f4f6'
      }}
      transition={{
        height: {
          type: "spring",
          stiffness: 300,
          damping: 30
        },
        backgroundColor: {
          duration: 0.2
        }
      }}
      style={{
        width: 300,
        borderRadius: 8,
        padding: 16,
        cursor: 'pointer',
        overflow: 'hidden'
      }}
      onClick={() => setIsExpanded(!isExpanded)}
    >
      <h3 style={{ margin: 0 }}>展開可能なカード</h3>
      {isExpanded && (
        <motion.p
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          transition={{ delay: 0.1 }}
          style={{ margin: '12px 0 0 0' }}
        >
          詳細な内容がここに表示されます。このテキストは展開時にフェードインします。
        </motion.p>
      )}
    </motion.div>
  )
}

プロパティごとに異なるトランジションを設定することで、より細かなアニメーション制御が可能です。高さにはバネ物理演算、背景色には線形遷移を適用しています。

以下の図は、initial・animate・exit の実行フローを示しています:

mermaidstateDiagram-v2
  [*] --> Initial: コンポーネントマウント
  Initial --> Animate: 自動遷移
  Animate --> StateChange: プロパティ変更
  StateChange --> Animate: 新しい状態へアニメーション
  Animate --> Exit: アンマウント開始
  Exit --> [*]: DOM から削除
  
  note right of Initial
    initial プロパティで定義
    マウント時の初期状態
  end note
  
  note right of Animate
    animate プロパティで定義
    目標となる状態
  end note
  
  note right of Exit
    exit プロパティで定義
    削除時のアニメーション
  end note

この状態遷移により、コンポーネントのライフサイクル全体を通してスムーズなアニメーションが実現されます。

図で理解できる要点:

  • initial → animate は自動的に実行される
  • animate 状態では、プロパティ変更に応じて新しいアニメーションが生成される
  • exit は AnimatePresence 内でのみ実行される

まとめ

Motion(旧 Framer Motion)の基本 API である motion 要素と initialanimateexit プロパティの使い方を詳しく解説いたしました。

重要なポイントをまとめると以下の通りです:

  • motion 要素: 通常の HTML 要素にアニメーション機能を追加
  • initial プロパティ: マウント時の初期状態を定義
  • animate プロパティ: 目標となる最終状態を宣言的に記述
  • exit プロパティ: AnimatePresence と組み合わせてアンマウント時のアニメーションを制御

Motion を活用することで、React アプリケーションに美しく滑らかなアニメーションを簡単に追加できます。宣言的な記述により、コードの可読性とメンテナンス性も大幅に向上するでしょう。

次のステップとして、より高度な機能である variantsgestures の活用、パフォーマンス最適化のテクニックを学んでいくことをお勧めいたします。

関連リンク