tech-book-labs
ガイド(UI / インタラクション) · 2 セクション統合

Framer Motion v12 アニメーション実装ガイド

Framer Motion v12 で UI アニメーションを組むパターン。基本の transition recipe、layoutId による magic move までを 1 ページに統合。

著者:TechBook.net編集部 · 最終検証 2026-05-10
セクション · 2026-05-10 · framer-motion 12.38.0

Framer Motion v12 で React にアニメーションを足す

Framer Motion v12 系で variants / AnimatePresence / whileHover を最小コードで使い分けるパターン、spring transition のチューニング、よく詰まる点を触れる demo で確認する実装メモ。

検証日: 2026-05-10

使用バージョン: framer-motion@12.38.0

対象: React で出現 / 切替 / hover アニメーションを宣言的に書きたい場面

Framer Motion v12 で variants / AnimatePresence / whileHover を最小コードで使い分けるパターン。spring transition の tuning、stagger / LazyMotion / useReducedMotion までを動く demo で確認します。

触って試す

variant:
variant: fade
hover で拡大します

variant ボタンで効果切替、show/hide で出現・退場、カードホバーで scale。

動きの “状態機械”

Framer Motion は内部で各 motion component を 3 つの アニメーション状態(initial / animate / exit)で扱います。AnimatePresence がマウント解除を遅延させるため、exit の演出が走り切ってから DOM が消えます。

motion component の状態遷移。animate 状態の中に whileHover / whileTap がネスト (クリックで拡大)

ここを掴んでおくと、「exit が走らない」「hover 中に animate prop が変わったらどうなる」のような疑問は自力で追えるようになります。

最小サンプル

motion.div<div> の代わりに使い、initialanimate の差分を Framer が自動補完:

import { motion } from "framer-motion";

export function Card() {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.3 }}
    >
      Hello
    </motion.div>
  );
}

<motion.div /><div /> を完全に置き換えられる(子・class・ref 透過)。

variant パターン

繰り返し使うアニメーションは variants に切り出し:

const fadeUp = {
  initial: { opacity: 0, y: 20 },
  animate: { opacity: 1, y: 0 },
  exit: { opacity: 0, y: -20 },
};

<motion.div variants={fadeUp} initial="initial" animate="animate" exit="exit" />

または直接展開する書き方も:

<motion.div {...fadeUp} />

退場アニメーション(AnimatePresence)

要素が消える時にアニメーションするには AnimatePresence で囲む:

import { motion, AnimatePresence } from "framer-motion";

<AnimatePresence mode="wait">
  {show && (
    <motion.div
      key="card"
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
    >
      Hello
    </motion.div>
  )}
</AnimatePresence>

mode の選び方

AnimatePresencemode で「前の要素を待つか / 重ねるか / レイアウトを保つか」が変わります。

3 つの mode の違い。新旧要素のタイミング差で UI 体験が大きく変わる (クリックで拡大)
mode推奨用途
sync(default)切り替えで重なっても問題ない時(タブ間の小ボックス等)
waithero / モーダル / ページ切替。前後が重なると違和感
popLayout一覧から要素を抜く時、隣接要素を「詰めずに」演出

mode="wait" で前の要素退場 → 次の要素入場の順序を直列化。popLayout は要素削除時に layout prop と組み合わせる と隣接要素の自動シフトが滑らかになる。

hover / tap

whileHover / whileTap を prop に書くだけで、対応する pointer event 中だけ別 state に切り替わる:

<motion.button whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} />

prop に書くだけで CSS transition より滑らか + transformGPU で軽い。

transition の選択

物理ベースの spring と、時間指定の duration どちらを使うかで体感が変わる:

transition={{ type: "spring", stiffness: 200, damping: 18 }}
// 物理ベース、自然な動き

transition={{ duration: 0.3, ease: "easeOut" }}
// duration ベース、コントロールしやすい

UI のフィードバック(button / card)は spring がしっくり来やすい。

spring パラメータの感覚

paramデフォルト効果チューニングの目安
stiffness100バネの硬さ → 速さUI 用は 200–400(キビキビ)、装飾は 80–120(ゆったり)
damping10減衰 → 揺れ戻り過減衰なら高め(20–30)、わずかに揺れたければ 8–14
mass1重さ重さで遅延感を出したい時のみ。通常 1 のまま

経験則:stiffness / damping ≈ 10 前後 が 1 度小さく揺れて止まる「ちょうどいい」UI 動き。dampingstiffness に対して大きくすれば「シュッ」と止まる、小さくすれば「ぽよん」と揺れる。

stagger でリストを連鎖入場

一覧の交互入場は 親の staggerChildren + 子 variants で順序感を作ります。

stagger preset:items:
staggerChildren: 0.08s
  • 🌱 アイテム 1
  • 🪴 アイテム 2
  • 🌿 アイテム 3
  • 🌳 アイテム 4
  • 🍀 アイテム 5
  • 🌻 アイテム 6

preset で stagger 速度・方向を切替、items で要素数を変えて視覚的に確認できます。

最小コード:

const list = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: { staggerChildren: 0.08, delayChildren: 0.05 },
  },
};

const item = {
  hidden: { opacity: 0, y: 16 },
  visible: { opacity: 1, y: 0, transition: { type: "spring", stiffness: 300, damping: 24 } },
};

<motion.ul variants={list} initial="hidden" animate="visible">
  {items.map((i) => (
    <motion.li key={i} variants={item}>{i}</motion.li>
  ))}
</motion.ul>

ポイント:

  • 親に variants を付けると、子も同じ variant 名(hidden / visible)を共有して再帰的に伝播
  • 子で initial/animate を直接書く必要なし — 親の状態を継承するだけで効く
  • staggerDirection: -1 で逆順入場(末尾から)
  • 逆退場したい時は exit 側にも別の staggerChildren を入れる(demo は退場時 60% 速で stagger)

バンドル削減: LazyMotion + m コンポーネント

全部入りの motion を import すると framer-motion の機能が全部 bundle に乗って 80KB 超え。LazyMotion で機能セットを限定 + m コンポーネントで参照を短く:

import { LazyMotion, domAnimation, m } from "framer-motion";

export function App() {
  return (
    <LazyMotion features={domAnimation}>
      {/* この内側では <m.div /> が <motion.div /> の代わり */}
      <m.div animate={{ opacity: 1 }} />
    </LazyMotion>
  );
}
feature pack含まれるものサイズ感(gzip)
domAnimationbase animation, variants, exit, gesture約 25KB
domMax上記 + drag, layout, layoutId約 35KB

ルートで <LazyMotion> で囲む + 全アニメ component を m.* に置換 → サイズ 1/3 程度に。

アクセシビリティ: useReducedMotion

OS の「動きの軽減」設定を検出して、装飾アニメだけを止めるパターン。useReducedMotion() が真なら initial: false + duration: 0 で即着地:

import { useReducedMotion, motion } from "framer-motion";

export function FadeIn({ children }) {
  const reduce = useReducedMotion();
  return (
    <motion.div
      initial={reduce ? false : { opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={reduce ? { duration: 0 } : { duration: 0.3 }}
    >
      {children}
    </motion.div>
  );
}

OS 設定で 動きの軽減を ON にしているユーザーには:

  • initialfalse にして「いきなり最終状態」で出す
  • transition.duration を 0 にしてジャンプさせる

「装飾アニメは reducedMotion で止める、機能上必須なフィードバック(ドラッグ追従等)は維持」が原則。

つまずいたポイント

  • AnimatePresence の子に key 必須 — 同じ component でも key が変わらないと exit が走らない。タブ切替 / モーダル / ルート切替で頻発
  • exit を効かせるには AnimatePresence で囲む<motion.div exit={...} /> 単体では unmount 即時で何も起きない
  • exit で stagger が効かない — 親 variants の transition.staggerChildrenexit 用にも別途指定する必要がある(子が先にすべて消えた後に親が unmount するため)
  • state 切替時に initial が走らない — Framer は「マウント直後だけ initial → animate 遷移」の挙動。動的に切替たい場合は key を付けて再 mount させる
  • layout prop と CSS transform の併用で意図しない動きmotion.div style={{ transform: ... }}layout を同時使用すると競合。style の transform は避けて animate 経由で
  • list reordering で全要素が再アニメkey が安定していれば最小限の差分だけ動く。array index を key にすると順序変化で全要素が exit + initial を起こす
  • gestures の whileHover が hover 中に animate を上書きされて止まるwhileHoveranimate より優先度が低い。state 切替で animate を変えると hover 効果が消えるので、hover 中は state 変更を抑制するか useAnimate 等の imperative API に切替
  • bundle サイズ — 全機能で 80KB+。m モジュール + LazyMotion で 1/3 程度に削れる(上記参照)
  • layoutId で magic move — 同じ id の要素が場所を変えると、Framer がレイアウトアニメーションを自動補完。一覧 → 詳細の expand 演出に強い(/articles/framer-motion-v12-layoutid-magic-move/ 参照)

評価

観点評価コメント
学習コスト<motion.div animate={...} /> だけで開始可
表現力spring / stagger / layoutId / 3D まで幅広い
バンドル全部入りは 80KB+、LazyMotion で削れる
パフォーマンスtransformGPU、再描画最適化
TypeScript型推論強い、editor 補完が効く

向く / 向かないケース

  • 向く: ページ遷移、modal 出現、list の stagger 入場、UX フィードバック
  • 向かない: 60fps クリティカルなゲーム / canvas アニメ(react-three-fiber + GPU)
  • 向かない: SEO 用の単純な fade(CSS transition で十分、bundle 削減)

関連 Topic / 関連書籍

この記事と関係する tech-book.net の Topic と、それぞれの Topic に紐づく書籍:

tech-book.net /books?isbn=9784839966645

React Native+Expoではじめるスマホアプリ開発 : JavaScriptによるアプリ構築の実際

松澤 太郎 · マイナビ出版 · 2018年

「React Native」は、Facebookが開発しているスマートフォンアプリ向けの開発環境です。ほとんどのコードをJav…

詳細を tech-book.net で見る
tech-book.net /books?isbn=9784873119380

Reactハンズオンラーニング 第2版 : Webアプリケーション開発のベストプラクティス

Alex Banks/Eve Porcello/宮崎 空 · オライリー・ジャパン · 2021年 · ¥3,740

Webフロントエンドの「今」を学びたい人へ! Facebookが開発したJavaScrip…

詳細を tech-book.net で見る
tech-book.net /books?isbn=9784873117881

Reactビギナーズガイド : コンポーネントベースのフロントエンド開発入門

Stoyan Stefanov/牧野 聡 · オライリー・ジャパン · 2017年 · ¥2,750

FacebookのエンジニアによるReactの入門書! ReactによるコンポーネントベースのWebフロントエンド開発の…

詳細を tech-book.net で見る
tech-book.net /books?isbn=9784297129163

TypeScriptとReact/Next.jsでつくる実践Webアプリケーション開発

手島 拓也/吉田 健人/高林 佳稀 · 技術評論社 · 2022年

新しいフロントエンドの入門書決定版! 本書はReact/Next.jsとTypeScriptを用いてWebアプリケーションを開…

詳細を tech-book.net で見る
tech-book.net /books?isbn=9784844379546

【POD】React &amp; Gatsby開発入門

竹本 雄貴
詳細を tech-book.net で見る
セクション · 2026-05-10 · framer-motion 12.38.0

Framer Motion v12 の layoutId で magic move + LazyMotion で削る

Framer Motion v12 の layoutId で grid → 詳細展開を魔法のように繋ぐ実装、LayoutGroup の使いどころ、LazyMotion + domAnimation で bundle を 1/3 に減らす Tree-shake パターンを触れる demo で確認する実装メモ。

検証日: 2026-05-10

使用バージョン: framer-motion@12.38.0

対象: 入門の <motion.div animate={...} /> は書けた、shared element transition と bundle 削減を覚えたい人

同じ layoutId を持つ別要素間で「グリッド → 詳細展開」を魔法のように繋ぐ magic move と、LazyMotion + domAnimation で bundle を 1/3 に減らす tree-shake パターンを動く demo で確認します。

触って試す

カードクリックで詳細展開(layoutId による magic move + AnimatePresence)

カードをクリックすると詳細パネルへと同じ要素が移動して拡大するように見える。実装は 同じ layoutId を持つ別の <motion.div> を出すだけ

1. layoutId とは

layoutId="card-1" を 2 つの <motion.div> に同時に付け、片方が unmount された瞬間にもう片方を mount すると、Framer Motion が 位置・サイズ・形 をスムーズに繋いでくれる。

// Step 1: グリッド側
<motion.div layoutId="card-1" onClick={() => setOpen("1")}>
  <motion.h3 layoutId="title-1">記事タイトル</motion.h3>
</motion.div>

// Step 2: 開いた時(別の場所に同じ layoutId)
{open === "1" && (
  <motion.div layoutId="card-1" className="...detail-position">
    <motion.h3 layoutId="title-1" className="...big">記事タイトル</motion.h3>
    <p>詳細本文</p>
  </motion.div>
)}

ポイント:

  • 片方 unmount + もう片方 mount で transition が走る
  • 入れ子の <motion.h3 layoutId> も同じ id ペアで個別に補完される
  • AnimatePresence でラップしないと unmount アニメーションが効かない
layoutId による magic move の内部処理 — measure 2 回 + 差分を transform で補完 (クリックで拡大)

2. AnimatePresence + layoutId の組み合わせ

<LayoutGroup> で全体を囲み、grid 側 / 詳細側に 同じ layoutId を持つ要素を出し分ける。<AnimatePresence> で unmount アニメも有効化:

import { motion, AnimatePresence, LayoutGroup } from "framer-motion";

<LayoutGroup>
  <div className="grid grid-cols-4 gap-4">
    {cards.map((c) => (
      <motion.button
        key={c.id}
        layoutId={`card-${c.id}`}
        onClick={() => setOpen(c.id)}
      >
        <motion.h3 layoutId={`title-${c.id}`}>{c.title}</motion.h3>
      </motion.button>
    ))}
  </div>

  <AnimatePresence>
    {opened && (
      <motion.div
        layoutId={`card-${opened}`}
        className="modal-position"
      >
        <motion.h3 layoutId={`title-${opened}`} className="big">
          {opened.title}
        </motion.h3>
        <motion.p initial={{ opacity: 0 }} animate={{ opacity: 1, transition: { delay: 0.15 } }}>
          {opened.body}
        </motion.p>
      </motion.div>
    )}
  </AnimatePresence>
</LayoutGroup>

ポイント:

  • <LayoutGroup> で囲う:複数の layoutId を同期させて滑らかに繋ぐ
  • 詳細側の追加要素(本文 p 等)は delay を入れて magic move の後にフェードイン
  • exit で逆再生:AnimatePresence 内の要素は exit で grid 位置に戻る

3. magic move が破綻するケース

症状原因
飛ばない同じ layoutId のペアが見つからない(typo / map の key 漏れ)
跳ねる親の transform / scroll の中で計算が壊れる(scrollY を考慮しない)
詰まる大量カードを LayoutGroup で束ねすぎ(数百以上だと measure コスト)
ガクつくunmount → mount の間に gap があると一瞬消える(重複 mount を避ける)

実装は ID の整合性が 9 割。

4. LazyMotion で bundle を削る

framer-motion 全部入りは min+gzip で 80KB+。実際に使う機能だけにすると 1/3 程度に。

import { LazyMotion, domAnimation, m } from "framer-motion";

// motion.div の代わりに m.div
<LazyMotion features={domAnimation}>
  <m.div animate={{ opacity: 1 }} />
</LazyMotion>

ポイント:

  • m から import(motion だと全機能が tree-shake されない)
  • features={domAnimation} = 標準アニメーション一式(animate / variants / transition)
  • features={domMax} = + drag / layout(layoutId は domMax 必要)
  • import を 1 箇所にまとめて wrapper 化:プロジェクト全体で m のみ使うように規約化

domMax 使う時のサイズ感:

構成概算サイズ(min+gzip)
motion 全部入り~80 KB
LazyMotion + domAnimation~25 KB
LazyMotion + domMax~50 KB

layoutId / drag を使うと domMax なので 50KB ほどに。framer-motion 比で 30% 削減。

LazyMotion の features pack 比較 — layoutId 使用時は domMax を選ぶ (クリックで拡大)

5. spring 物理 vs duration の使い分け

UI フィードバックは spring(自然な減衰)、一覧の入場は duration(順序感)、magic move は spring 推奨(物理的に繋がって見える):

// UI フィードバック(button / card / modal)→ spring 推奨
transition={{ type: "spring", stiffness: 200, damping: 18 }}

// 一覧の stagger 入場 → duration
transition={{ duration: 0.3, ease: "easeOut" }}

// magic move → デフォルトの spring が自然
// (transition を省略すると自動で spring)

ポイント:

  • spring の stiffness 高い = 速く減衰、damping 高い = オーバーシュート少ない
  • 推奨初期値:stiffness: 200, damping: 18(Apple っぽい質感)
  • duration 派は ease: "easeOut" で「最後にゆっくり」が UX 自然

6. reducedMotion(アクセシビリティ)

useReducedMotion() で OS 設定を検出し、true なら transition の duration を 0 にして即着地させる:

import { useReducedMotion } from "framer-motion";

function Card() {
  const reduced = useReducedMotion();
  return (
    <motion.div
      animate={{ opacity: 1 }}
      transition={reduced ? { duration: 0 } : { type: "spring" }}
    />
  );
}

OS 設定で reduced motion を有効にしているユーザは、アニメーションを完全に止める のが UX 配慮。

つまずいたポイント

  • 同じ layoutId が同時に 2 つ mount している:両方が同時に存在する瞬間があると、Framer Motion が「どっちに繋ぐか」迷う。AnimatePresence mode="wait" を使うか、unmount を完全にしてから mount
  • whileHover={{ scale }}layout の競合:layout 中は scale が無視されることがある、whileHover の代わりに transition 内の whileHover state を別管理
  • scroll 中の magic move 計算ずれ:<LayoutGroup> の親が scroll コンテナだと位置計算がずれる、<LayoutGroup id="stable-id"> で安定 group key を持たせる
  • domAnimation で layoutId が動かない:layoutId は drag / layout features の一部、domMax を選ぶ
  • m.divmotion.div の混在:m を使うなら全部 m、混ぜると LazyMotion 効果が減る

関連 Topic / 関連書籍

この記事と関係する tech-book.net の Topic と、それぞれの Topic に紐づく書籍:

tech-book.net /books?isbn=9784839966645

React Native+Expoではじめるスマホアプリ開発 : JavaScriptによるアプリ構築の実際

松澤 太郎 · マイナビ出版 · 2018年

「React Native」は、Facebookが開発しているスマートフォンアプリ向けの開発環境です。ほとんどのコードをJav…

詳細を tech-book.net で見る
tech-book.net /books?isbn=9784873119380

Reactハンズオンラーニング 第2版 : Webアプリケーション開発のベストプラクティス

Alex Banks/Eve Porcello/宮崎 空 · オライリー・ジャパン · 2021年 · ¥3,740

Webフロントエンドの「今」を学びたい人へ! Facebookが開発したJavaScrip…

詳細を tech-book.net で見る
tech-book.net /books?isbn=9784873117881

Reactビギナーズガイド : コンポーネントベースのフロントエンド開発入門

Stoyan Stefanov/牧野 聡 · オライリー・ジャパン · 2017年 · ¥2,750

FacebookのエンジニアによるReactの入門書! ReactによるコンポーネントベースのWebフロントエンド開発の…

詳細を tech-book.net で見る
tech-book.net /books?isbn=9784297129163

TypeScriptとReact/Next.jsでつくる実践Webアプリケーション開発

手島 拓也/吉田 健人/高林 佳稀 · 技術評論社 · 2022年

新しいフロントエンドの入門書決定版! 本書はReact/Next.jsとTypeScriptを用いてWebアプリケーションを開…

詳細を tech-book.net で見る
tech-book.net /books?isbn=9784844379546

【POD】React &amp; Gatsby開発入門

竹本 雄貴
詳細を tech-book.net で見る

全ガイドは ガイド一覧 から。連載は 連載一覧 へ。