T-CREATOR

Motion(旧 Framer Motion)アニメオーケストレーション設計:timeline・遅延・相互依存の整理術

Motion(旧 Framer Motion)アニメオーケストレーション設計:timeline・遅延・相互依存の整理術

複数のアニメーション要素を美しく連動させることは、モダンな Web アプリケーションにおいて重要な要素です。Motion(旧 Framer Motion)を使えば、複雑なアニメーションシーケンスを直感的に組み立てられますが、timeline 設計や遅延制御、要素間の依存関係を適切に管理しなければ、コードが複雑化してしまいます。

本記事では、Motion のアニメーションオーケストレーション機能を体系的に整理し、実務で使える設計パターンをご紹介します。

背景

Motion のオーケストレーション機能とは

Motion は、React 向けのアニメーションライブラリとして、単一要素のアニメーションだけでなく、複数の要素を連携させる「オーケストレーション機能」を提供しています。

この機能を使うことで、以下のような表現が可能になります。

  • 複数の要素を順番にアニメーションさせる
  • 親要素の動きに合わせて子要素を連動させる
  • 特定の要素の完了を待って次の要素を動かす
  • タイムライン上で複数のアニメーションを同期させる

以下の図は、Motion のオーケストレーション機能における基本的な制御の流れを示しています。

mermaidflowchart TB
  parent["親コンポーネント<br/>(motion.div)"]
  child1["子要素1<br/>(motion.div)"]
  child2["子要素2<br/>(motion.div)"]
  child3["子要素3<br/>(motion.div)"]

  parent -->|"variants適用"| child1
  parent -->|"variants適用"| child2
  parent -->|"variants適用"| child3

  child1 -->|"transition.delay"| child2
  child2 -->|"transition.delay"| child3

  style parent fill:#e1f5ff
  style child1 fill:#fff4e1
  style child2 fill:#fff4e1
  style child3 fill:#fff4e1

上図のように、親要素から子要素へ variants を伝播させることで、統一されたアニメーション制御が実現できます。

オーケストレーションが必要とされる場面

実務では、以下のようなシーンでオーケストレーション設計が求められます。

  • UI 要素の段階的表示: メニューやモーダルの開閉時に、各要素を順番に表示する
  • ローディングアニメーション: 複数のスケルトンやプログレスバーを連動させる
  • ページ遷移: コンテンツがフェードアウトしてから次のページがフェードインする
  • データビジュアライゼーション: グラフや図表の各パーツを順番に描画する
  • スクロール連動アニメーション: スクロール位置に応じて複数の要素を段階的に動かす

これらのシーンでは、単純なtransitionプロパティだけでは対応が難しく、体系的な設計アプローチが必要になります。

課題

アニメーション制御が複雑化する要因

Motion でアニメーションオーケストレーションを実装する際、以下のような課題に直面することがあります。

課題 1:遅延タイミングの管理が煩雑

複数の要素に異なる遅延時間を設定する場合、ハードコーディングされた数値が散在しがちです。

typescript// アンチパターン:遅延時間がハードコーディングされている
<motion.div transition={{ delay: 0.2 }} />
<motion.div transition={{ delay: 0.4 }} />
<motion.div transition={{ delay: 0.6 }} />

このような実装では、アニメーション全体のタイミングを調整する際に、すべての要素を手動で修正する必要があります。

課題 2:親子関係の依存が明示的でない

親要素のアニメーション完了を待ってから子要素を動かしたいケースで、制御フローが不明瞭になることがあります。

typescript// 親と子の依存関係が見えにくい
const parent = {
  initial: { opacity: 0 },
  animate: { opacity: 1, transition: { duration: 0.5 } },
};

const child = {
  initial: { y: 20 },
  animate: { y: 0, transition: { delay: 0.5 } }, // なぜ0.5秒?
};

上記のコードでは、子要素の遅延時間(0.5 秒)が親要素の継続時間と一致していることが、コメントなしでは理解しにくくなっています。

課題 3:複雑なタイムラインの可視化が困難

多数の要素が絡むアニメーションでは、全体のタイムラインを把握することが難しくなります。

以下の図は、アニメーションタイムラインが複雑化する様子を示しています。

mermaidsequenceDiagram
  participant Header as ヘッダー
  participant Nav as ナビゲーション
  participant Content as コンテンツ
  participant Footer as フッター

  Note over Header,Footer: ページ読み込み開始

  Header->>Header: フェードイン (0ms-300ms)

  Note over Nav: Header完了後
  Nav->>Nav: スライドイン (300ms-600ms)

  Note over Content: Nav完了後
  Content->>Content: フェードイン (600ms-900ms)

  Note over Footer: Content完了後
  Footer->>Footer: フェードイン (900ms-1200ms)

このような連鎖的なアニメーションは、コードだけでは全体像を把握しにくく、保守性が低下します。

課題 4:再利用可能なパターンの欠如

プロジェクト全体で統一されたアニメーションパターンがないと、各開発者が独自の実装をしてしまい、コードの一貫性が失われます。

これらの課題を解決するには、体系的なオーケストレーション設計パターンが必要です。

解決策

オーケストレーション設計の 3 つの柱

Motion のアニメーションオーケストレーションを整理するには、以下の 3 つの要素を体系的に管理します。

#要素役割主な機能
1Variants状態定義とタイムライン制御staggerChildrendelayChildren
2Transition個別アニメーションのタイミング制御delaydurationwhen
3Orchestration要素間の依存関係管理onAnimationComplete、カスタムフック

これらを組み合わせることで、複雑なアニメーションを明瞭に設計できます。

設計パターン 1:Variants によるカスケード制御

Variants を使うと、親要素から子要素へアニメーション状態を伝播させられます。

基本的な Variants 定義

typescript// 親要素のvariants定義
const containerVariants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      staggerChildren: 0.1, // 子要素を0.1秒ずつ遅延
      delayChildren: 0.3, // 最初の子要素を0.3秒遅延
    },
  },
};

このコードでは、staggerChildrenで子要素を順番に表示し、delayChildrenで全体の開始タイミングを制御しています。

子要素の Variants 定義

typescript// 子要素のvariants定義
const itemVariants = {
  hidden: {
    opacity: 0,
    y: 20,
  },
  visible: {
    opacity: 1,
    y: 0,
    transition: {
      duration: 0.5,
      ease: 'easeOut',
    },
  },
};

子要素では、親から伝播されるhiddenvisible状態に対応するアニメーションを定義します。

コンポーネントへの適用

typescriptimport { motion } from 'motion/react';

export function StaggeredList() {
  return (
    <motion.ul
      variants={containerVariants}
      initial='hidden'
      animate='visible'
    >
      {items.map((item) => (
        <motion.li key={item.id} variants={itemVariants}>
          {item.text}
        </motion.li>
      ))}
    </motion.ul>
  );
}

このように、親要素にvariantsinitialanimateを設定するだけで、子要素に自動的にアニメーションが適用されます。

以下の図は、Variants によるカスケード制御の流れを示しています。

mermaidflowchart LR
  container["motion.ul<br/>(containerVariants)"]
  item1["motion.li[0]<br/>(itemVariants)"]
  item2["motion.li[1]<br/>(itemVariants)"]
  item3["motion.li[2]<br/>(itemVariants)"]

  container -->|"delayChildren: 0.3s"| item1
  item1 -->|"staggerChildren: 0.1s"| item2
  item2 -->|"staggerChildren: 0.1s"| item3

  note1["t=0.3s<br/>opacity: 0→1"]
  note2["t=0.4s<br/>opacity: 0→1"]
  note3["t=0.5s<br/>opacity: 0→1"]

  item1 -.->|アニメーション| note1
  item2 -.->|アニメーション| note2
  item3 -.->|アニメーション| note3

上図のように、親要素の設定だけで子要素のタイミングが自動的に決定されるため、管理が容易になります。

設計パターン 2:Transition による詳細な制御

Variants だけでは不十分な場合、transitionプロパティで詳細な制御を行います。

複数プロパティの個別制御

typescript// 異なるプロパティに異なるタイミングを適用
const complexVariants = {
  hidden: {
    opacity: 0,
    scale: 0.8,
    y: 50,
  },
  visible: {
    opacity: 1,
    scale: 1,
    y: 0,
    transition: {
      // opacityとscaleを先に完了させる
      opacity: { duration: 0.3 },
      scale: { duration: 0.3 },
      // yは遅れて開始し、長めに実行
      y: { delay: 0.2, duration: 0.6, ease: 'easeOut' },
    },
  },
};

このコードでは、フェードイン・スケールアップを先に完了させてから、スライドアニメーションを実行しています。

when オプションによる順序制御

typescript// アニメーション完了を待ってから次を実行
const sequentialVariants = {
  hidden: { opacity: 0, x: -100 },
  visible: {
    opacity: 1,
    x: 0,
    transition: {
      opacity: { duration: 0.3 },
      // opacityが完了してからxを実行
      x: { duration: 0.5 },
      when: 'beforeChildren', // 子要素のアニメーション前に完了
    },
  },
};

whenオプションを使うことで、親子間のアニメーション順序を明示的に制御できます。

利用可能な when オプション

#オプション動作使用場面
1beforeChildren親が完了してから子を開始モーダルの背景 → コンテンツ
2afterChildren子が完了してから親を開始子要素の読み込み → 全体表示
3未指定(デフォルト)親と子が同時に開始通常のカスケード表示

設計パターン 3:相互依存の管理

複数の独立した要素が相互に依存する場合、状態管理とコールバックを組み合わせます。

状態管理による連鎖制御

typescriptimport { useState } from "react";
import { motion } from "motion/react";

export function SequentialAnimation() {
  const [step, setStep] = useState(0);

  return (
    <>
      {/* ステップ1 */}
      <motion.div
        initial={{ opacity: 0 }}
        animate={{ opacity: step >= 0 ? 1 : 0 }}
        onAnimationComplete={() => setStep(1)}
      >
        最初の要素
      </motion.div>

この実装では、状態変数stepによってアニメーションの進行を管理しています。

コールバックによる次ステップのトリガー

typescript      {/* ステップ2 */}
      <motion.div
        initial={{ x: -50, opacity: 0 }}
        animate={{
          x: step >= 1 ? 0 : -50,
          opacity: step >= 1 ? 1 : 0
        }}
        onAnimationComplete={() => setStep(2)}
      >
        次の要素
      </motion.div>

      {/* ステップ3 */}
      <motion.div
        initial={{ y: 20, opacity: 0 }}
        animate={{
          y: step >= 2 ? 0 : 20,
          opacity: step >= 2 ? 1 : 0
        }}
      >
        最後の要素
      </motion.div>
    </>
  );
}

onAnimationCompleteコールバックで次のステップをトリガーすることで、厳密な順序制御が可能になります。

カスタムフックによる再利用可能な設計

typescript// アニメーションステップを管理するカスタムフック
import { useState, useCallback } from 'react';

export function useAnimationSequence(totalSteps: number) {
  const [currentStep, setCurrentStep] = useState(0);

  const nextStep = useCallback(() => {
    setCurrentStep((prev) =>
      prev < totalSteps - 1 ? prev + 1 : prev
    );
  }, [totalSteps]);

  const isStepActive = useCallback(
    (step: number) => {
      return currentStep >= step;
    },
    [currentStep]
  );

  return { currentStep, nextStep, isStepActive };
}

このフックは、アニメーションシーケンス全体のステップ管理を抽象化しています。

カスタムフックの利用例

typescriptexport function ImprovedSequentialAnimation() {
  const { nextStep, isStepActive } =
    useAnimationSequence(3);

  return (
    <>
      <motion.div
        animate={{ opacity: isStepActive(0) ? 1 : 0 }}
        onAnimationComplete={nextStep}
      >
        最初の要素
      </motion.div>

      <motion.div
        animate={{ opacity: isStepActive(1) ? 1 : 0 }}
        onAnimationComplete={nextStep}
      >
        次の要素
      </motion.div>

      <motion.div
        animate={{ opacity: isStepActive(2) ? 1 : 0 }}
      >
        最後の要素
      </motion.div>
    </>
  );
}

カスタムフックを使うことで、コードの可読性と再利用性が大幅に向上します。

具体例

実例 1:モーダルの段階的表示

モーダルを開く際に、背景オーバーレイ → コンテナ → 各要素の順で表示する実装です。

型定義とインターフェース

typescript// モーダルのプロパティ定義
interface ModalProps {
  isOpen: boolean;
  onClose: () => void;
  children: React.ReactNode;
}

interface ModalContentItem {
  id: string;
  content: React.ReactNode;
}

型定義によって、コンポーネントの仕様が明確になります。

Variants 定義

typescript// 背景オーバーレイのvariant
const overlayVariants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: { duration: 0.2 },
  },
};

// モーダルコンテナのvariant
const modalVariants = {
  hidden: {
    opacity: 0,
    scale: 0.8,
    y: 50,
  },
  visible: {
    opacity: 1,
    scale: 1,
    y: 0,
    transition: {
      duration: 0.3,
      delay: 0.1, // オーバーレイの後に開始
      when: 'beforeChildren', // 子要素の前に完了
    },
  },
};

各レイヤーのアニメーションタイミングを、delaywhenで明確に定義しています。

コンテンツ要素の Variants

typescript// モーダル内コンテンツのvariant
const contentVariants = {
  hidden: { opacity: 0, y: 20 },
  visible: {
    opacity: 1,
    y: 0,
    transition: {
      staggerChildren: 0.08, // 各要素を0.08秒ずつ遅延
      delayChildren: 0.1, // 全体を0.1秒遅延
    },
  },
};

const itemVariants = {
  hidden: { opacity: 0, x: -20 },
  visible: {
    opacity: 1,
    x: 0,
    transition: { duration: 0.4 },
  },
};

staggerChildrenによって、コンテンツが滑らかに順番に表示されます。

typescriptimport { motion, AnimatePresence } from "motion/react";

export function Modal({ isOpen, onClose, children }: ModalProps) {
  return (
    <AnimatePresence>
      {isOpen && (
        <>
          {/* 背景オーバーレイ */}
          <motion.div
            variants={overlayVariants}
            initial="hidden"
            animate="visible"
            exit="hidden"
            onClick={onClose}
            style={{
              position: "fixed",
              inset: 0,
              backgroundColor: "rgba(0, 0, 0, 0.5)"
            }}
          />

AnimatePresenceを使うことで、モーダルの開閉時に自動的にアニメーションが適用されます。

モーダルコンテンツの実装

typescript          {/* モーダルコンテナ */}
          <motion.div
            variants={modalVariants}
            initial="hidden"
            animate="visible"
            exit="hidden"
            style={{
              position: "fixed",
              top: "50%",
              left: "50%",
              transform: "translate(-50%, -50%)",
              backgroundColor: "white",
              padding: "2rem",
              borderRadius: "8px"
            }}
          >
            {/* コンテンツリスト */}
            <motion.div variants={contentVariants}>
              {React.Children.map(children, (child, index) => (
                <motion.div key={index} variants={itemVariants}>
                  {child}
                </motion.div>
              ))}
            </motion.div>
          </motion.div>
        </>
      )}
    </AnimatePresence>
  );
}

この実装により、モーダルの開閉時に美しい段階的アニメーションが実現できます。

以下の図は、モーダル表示のアニメーションタイムラインを示しています。

mermaidsequenceDiagram
  participant User as ユーザー操作
  participant Overlay as 背景オーバーレイ
  participant Container as モーダルコンテナ
  participant Content as コンテンツ要素

  User->>Overlay: モーダル表示クリック

  Note over Overlay: 0ms - 200ms
  Overlay->>Overlay: フェードイン (opacity: 0→1)

  Note over Container: 100ms - 400ms
  Container->>Container: スケール&スライド<br/>(scale: 0.8→1, y: 50→0)

  Note over Content: 500ms -
  Container->>Content: 子要素アニメーション開始

  loop 各コンテンツ要素(0.08秒ずつ)
    Content->>Content: スライドイン (x: -20→0)
  end

このタイムラインにより、ユーザーは視覚的な階層を認識しながらモーダルを理解できます。

実例 2:データ読み込みのローディングシーケンス

API からデータを取得する際の、段階的なローディング表示の実装です。

データ取得状態の型定義

typescript// データ取得の状態を表す型
type LoadingState =
  | 'idle'
  | 'loading'
  | 'success'
  | 'error';

interface DataLoadingProps {
  state: LoadingState;
  progress?: number; // 0-100
  data?: any;
}

明確な型定義により、各状態での挙動が明瞭になります。

ローディングアニメーションの Variants

typescript// スピナーのvariant
const spinnerVariants = {
  loading: {
    rotate: 360,
    transition: {
      duration: 1,
      repeat: Infinity,
      ease: 'linear',
    },
  },
};

// プログレスバーのvariant
const progressVariants = {
  loading: {
    scaleX: 1,
    transition: {
      duration: 2,
      ease: 'easeInOut',
    },
  },
};

無限ループと進行状況を、それぞれ異なるアニメーションで表現しています。

ローディングコンポーネントの実装

typescriptexport function DataLoading({ state, progress = 0, data }: DataLoadingProps) {
  return (
    <AnimatePresence mode="wait">
      {/* ローディング状態 */}
      {state === "loading" && (
        <motion.div
          key="loading"
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
        >
          <motion.div
            variants={spinnerVariants}
            animate="loading"
            style={{
              width: "40px",
              height: "40px",
              border: "4px solid #e0e0e0",
              borderTopColor: "#3b82f6",
              borderRadius: "50%"
            }}
          />

AnimatePresencemode="wait"により、状態遷移時に前の要素が消えてから次の要素が表示されます。

プログレスバーとデータ表示

typescript          <motion.div
            initial={{ scaleX: 0 }}
            animate={{ scaleX: progress / 100 }}
            style={{
              width: "200px",
              height: "4px",
              backgroundColor: "#3b82f6",
              marginTop: "1rem",
              transformOrigin: "left"
            }}
          />
        </motion.div>
      )}

      {/* 成功状態 */}
      {state === "success" && (
        <motion.div
          key="success"
          initial={{ opacity: 0, y: 20 }}
          animate={{ opacity: 1, y: 0 }}
          exit={{ opacity: 0, y: -20 }}
          transition={{ duration: 0.4 }}
        >
          {data && <DataDisplay data={data} />}
        </motion.div>
      )}
    </AnimatePresence>
  );
}

各状態に対応したアニメーションを定義することで、ユーザーにわかりやすいフィードバックを提供できます。

実例 3:複雑なタイムライン制御

複数のセクションが連鎖的にアニメーションする、ランディングページの実装です。

セクション構成の定義

typescript// 各セクションの構成を定義
interface Section {
  id: string;
  title: string;
  content: string;
  delay: number; // 前のセクションからの遅延時間
}

const sections: Section[] = [
  {
    id: 'hero',
    title: 'ヒーロー',
    content: '...',
    delay: 0,
  },
  {
    id: 'features',
    title: '機能',
    content: '...',
    delay: 0.3,
  },
  {
    id: 'pricing',
    title: '料金',
    content: '...',
    delay: 0.5,
  },
  { id: 'cta', title: 'CTA', content: '...', delay: 0.4 },
];

データ駆動でセクションを定義することで、タイムラインの調整が容易になります。

タイムライン制御フックの実装

typescriptimport { useState, useEffect } from 'react';

export function useTimelineControl(sections: Section[]) {
  const [activeSections, setActiveSections] = useState<
    Set<string>
  >(new Set());

  useEffect(() => {
    let cumulativeDelay = 0;

    sections.forEach((section, index) => {
      cumulativeDelay += section.delay * 1000;

      setTimeout(() => {
        setActiveSections(
          (prev) => new Set([...prev, section.id])
        );
      }, cumulativeDelay);
    });
  }, [sections]);

  const isSectionActive = (sectionId: string) => {
    return activeSections.has(sectionId);
  };

  return { isSectionActive };
}

累積遅延時間を計算することで、各セクションの表示タイミングを正確に制御しています。

セクションコンポーネントの実装

typescript// 個別セクションのvariant
const sectionVariants = {
  hidden: {
    opacity: 0,
    y: 60,
    scale: 0.95,
  },
  visible: {
    opacity: 1,
    y: 0,
    scale: 1,
    transition: {
      duration: 0.6,
      ease: [0.22, 1, 0.36, 1], // カスタムイージング
    },
  },
};

interface SectionProps {
  section: Section;
  isActive: boolean;
}

function AnimatedSection({
  section,
  isActive,
}: SectionProps) {
  return (
    <motion.section
      variants={sectionVariants}
      initial='hidden'
      animate={isActive ? 'visible' : 'hidden'}
    >
      <h2>{section.title}</h2>
      <p>{section.content}</p>
    </motion.section>
  );
}

カスタムイージング関数を使うことで、より自然なアニメーションが実現できます。

ランディングページの実装

typescriptexport function LandingPage() {
  const { isSectionActive } = useTimelineControl(sections);

  return (
    <main>
      {sections.map((section) => (
        <AnimatedSection
          key={section.id}
          section={section}
          isActive={isSectionActive(section.id)}
        />
      ))}
    </main>
  );
}

このシンプルな実装で、複雑なタイムライン制御を実現できます。

以下の図は、ランディングページの各セクションのアニメーションタイミングを示しています。

mermaidflowchart TB
  start["ページ読み込み"]
  hero["ヒーローセクション<br/>(delay: 0s)"]
  features["機能セクション<br/>(delay: 0.3s)"]
  pricing["料金セクション<br/>(delay: 0.5s)"]
  cta["CTAセクション<br/>(delay: 0.4s)"]

  start -->|"0.0s"| hero
  hero -->|"+0.3s = 0.3s"| features
  features -->|"+0.5s = 0.8s"| pricing
  pricing -->|"+0.4s = 1.2s"| cta

  style start fill:#e1f5ff
  style hero fill:#d4edda
  style features fill:#d4edda
  style pricing fill:#d4edda
  style cta fill:#d4edda

上図のように、各セクションの累積遅延時間が視覚的に把握できることで、タイムライン全体の調整が容易になります。

図で理解できる要点

本章では、以下の 3 つの実例を通じて、オーケストレーション設計を実践的に学びました。

  • モーダルは背景 → コンテナ → 要素の 3 層構造でアニメーションを設計する
  • データローディングは状態ごとに異なるアニメーションパターンを適用する
  • 複雑なタイムラインは累積遅延時間を計算して管理する

まとめ

Motion のアニメーションオーケストレーション設計には、以下の 3 つの要素が重要です。

設計の 3 つの柱を活用する

Variantsによって状態とタイムラインを定義し、Transitionで詳細なタイミングを制御し、カスタムフックで相互依存を管理することで、複雑なアニメーションを明瞭に実装できます。

データ駆動でタイムラインを設計する

遅延時間やアニメーション設定をハードコーディングせず、配列やオブジェクトとして定義することで、保守性と可読性が向上します。

段階的な制御パターンを使い分ける

単純なカスケード表示にはstaggerChildrenを、厳密な順序制御には状態管理を、複雑なタイムラインにはカスタムフックを使うことで、適切な設計が可能になります。

これらの設計パターンを活用することで、美しく保守しやすいアニメーションオーケストレーションを実現できるでしょう。

関連リンク