framer-motion について
React と Next.js の開発者向けに、Motion(Framer Motion)のアニメーション実装を解説するガイドです。一般的なパターン、パフォーマンス上の注意点、アクセシビリティを扱います。
主な機能
- 01スクロール起点と段階的なアニメーション
- 02レイアウトと退出アニメーション
- 03ジェスチャー、ドラッグ、スプリングアニメーション
- 04MotionValues によるフレーム更新
- 05視差効果の低減設定への対応
活用シーン
- 01ページ要素にスクロール時の表示効果を追加
- 02条件付きコンテンツの削除をアニメーション化
- 03ドラッグやジェスチャー操作のコンポーネントを構築
- 04スクロール進捗バーやカウンターを作成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ schoepplake/ framer-motion-skill --skill framer-motionインストール後、Claude Code で/framer-motionを入力して読み込まれたか確認します。
詳細
Motion(Framer Motion)— アニメーションスキル
React と Next.js 向けの本番環境に対応したアニメーションパターンです。このスキルでは、Motion ライブラリ(v12 以降)を使って、正確でパフォーマンスに優れ、アクセシブルなアニメーションを記述する方法を説明します。
インポート
Motion は framer-motion から motion にブランド名を変更しました。どちらのパッケージ名も使用できますが、インポートパスが重要です。
// Client Components (standard React)
import { motion, AnimatePresence } from "motion/react"
// Next.js Server Components — use the client export
import * as motion from "motion/react-client"
// Legacy import (still works with the framer-motion package)
import { motion } from "framer-motion"
プロジェクトですでに framer-motion が依存関係として使われている場合は、一貫性を保つため、引き続き "framer-motion" からインポートしてください。インポート元を混在させないでください。
基本概念
motion.* コンポーネント
すべての HTML/SVG 要素には、対応する motion コンポーネントがあります。これらのコンポーネントはアニメーション用の props を受け取ります。
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
/>
重要: motion.* コンポーネントはクライアントコンポーネントです。Next.js App Router では、ファイルに "use client" を指定するか、クライアントコンポーネントでラップしてください。
MotionValues — 再レンダリングなしのアニメーション
useMotionValue は、React の再レンダリングを発生させずに更新される値を作成します。これは 60fps のアニメーションを実現する鍵です。
const x = useMotionValue(0)
const opacity = useTransform(x, [-200, 0, 200], [0, 1, 0])
return <motion.div style={{ x, opacity }} drag="x" />
ルール:
- フレームごとに変化する値(スクロール位置、ドラッグ位置、連続アニメーション)には
useMotionValueを使います。 - 他の MotionValues から値を導出するには
useTransformを使います(再レンダリングは発生しません)。 - フレームごとの更新に
useStateを使わないでください。毎フレーム再レンダリングが発生します。
トランジション
アニメーションが状態間をどのように移動するかを制御します。
// Spring (default for physical properties like x, y, scale)
transition={{ type: "spring", stiffness: 300, damping: 30 }}
// Tween (default for non-physical properties like opacity, color)
transition={{ type: "tween", duration: 0.3, ease: "easeInOut" }}
// Custom cubic bezier
transition={{ duration: 0.7, ease: [0.16, 1, 0.3, 1] }}
よく使うパターン
スクロールをトリガーにしたフェードイン
最もよく使われるアニメーションパターンです。whileInView を使い、IntersectionObserver を手動で使わないでください。
<motion.div
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.1 }}
transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
>
{children}
</motion.div>
viewport.once: true— 初回表示時にのみアニメーションします(マーケティングページで標準的です)。viewport.amount— 要素のうち、表示される必要がある割合です(0.1 = 10%)。viewport.margin— トリガー領域を広げます(例:"0px 0px 50px 0px")。
子要素を順番に表示する
variants を使って、子要素を1つずつアニメーションさせます。
const container = {
hidden: {},
show: { transition: { staggerChildren: 0.1 } },
}
const item = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0 },
}
<motion.ul variants={container} initial="hidden" whileInView="show" viewport={{ once: true }}>
{items.map((i) => (
<motion.li key={i} variants={item}>{i}</motion.li>
))}
</motion.ul>
Variants は伝播するため、親の状態変化は自動的に子要素へ反映されます。
AnimatePresence による終了アニメーション
条件付きでレンダリングされる要素をラップすると、削除される前に終了アニメーションを実行できます。
<AnimatePresence>
{isOpen && (
<motion.div
key="modal"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.2 }}
/>
)}
</AnimatePresence>
ルール:
- 子要素には一意の
keyが必要です。 exitprop で終了アニメーションを定義します。- 次の要素が入る前に終了アニメーションを完了させるには、
mode="wait"を使います。 - レイアウトを考慮したトランジションには
mode="popLayout"を使います。
スクロールに連動したアニメーション
アニメーションをスクロール位置に直接連動させます。
const { scrollYProgress } = useScroll()
const opacity = useTransform(scrollYProgress, [0, 0.5], [1, 0])
const scale = useTransform(scrollYProgress, [0, 1], [1, 0.8])
return <motion.div style={{ opacity, scale }} />
要素ごとにスクロールをトラッキングするには、次のようにします。
const ref = useRef(null)
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start end", "end start"], // when element enters/exits viewport
})
滑らかなスプリング値
MotionValues のトランジションを非常に滑らかにするには、useSpring を使います。
const scrollY = useMotionValue(0)
const smoothY = useSpring(scrollY, { stiffness: 100, damping: 30 })
連続アニメーション(useAnimationFrame)
グラデーションのきらめきやカウントなど、毎フレーム実行されるアニメーションには、次のようにします。
const progress = useMotionValue(0)
useAnimationFrame((time, delta) => {
const newValue = (time / 1000) % 100
progress.set(newValue)
})
const backgroundPosition = useTransform(progress, (p) => `${p}% 50%`)
return <motion.span style={{ backgroundPosition }} />
レイアウトアニメーション
レイアウトの変更を自動的にアニメーションさせます。
// Simple layout animation
<motion.div layout />
// Shared layout animation between components
<motion.div layoutId="hero-image" />
異なる2つのコンポーネントに同じ layoutId がある場合、Motion はそれらの間のアニメーションを行います(例:サムネイルから全画面表示への切り替え)。
ジェスチャーアニメーション
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.97 }}
whileFocus={{ outline: "2px solid #5227FF" }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
/>
ドラッグ
<motion.div
drag // enable both axes
drag="x" // constrain to x-axis
dragConstraints={{ left: -100, right: 100 }}
dragElastic={0.2}
onDragEnd={(e, info) => {
if (info.offset.x > 100) handleSwipeRight()
}}
/>
アクセシビリティ
useReducedMotion
ユーザーのモーション設定を必ず尊重してください。これは任意ではなく、アクセシビリティ上の要件です。
import { useReducedMotion } from "framer-motion"
function AnimatedComponent({ children }) {
const prefersReducedMotion = useReducedMotion()
if (prefersReducedMotion) {
return <div>{children}</div>
}
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
>
{children}
</motion.div>
)
}
パターン:useReducedMotion() を確認し、アニメーションを完全に省略するか、移動を伴わない不透明度のみの変化に抑えます。
パフォーマンスのルール
-
width、height、top、leftはアニメーションさせないでください。 レイアウトの再計算が発生します。代わりにtransformプロパティ(x、y、scale、rotate)を使います。 -
フレームごとの更新には MotionValues を使います。
useStateを使うと毎フレーム再レンダリングが発生します。MotionValues は DOM を直接更新します。 -
will-change: transformは Motion コンポーネントによって自動的に追加されます。 手動で追加しないでください。 -
**
layoutアニメーションは負荷が高いため、**すべての要素に使わず、必要な箇所でのみ使ってください。 -
box-shadowのアニメーションは避けてください。 代わりにfilter: drop-shadow()を使うか、疑似要素のシャドウの不透明度をアニメーションさせてください。 -
opacityとtransformを優先してください。 これらは GPU で合成され、別スレッドで実行されます。 -
インタラクティブな要素(ホバー、タップ、ドラッグ)には、トゥイーン/イージングよりスプリングトランジションのほうが自然です。 トゥイーンはスクロールをトリガーにした表示時のアニメーションに使います。
よくある間違い
| 間違い | 修正方法 |
|---|---|
ドラッグ位置に useState を使う | useMotionValue を使います |
IntersectionObserver を手動で使う | whileInView プロパティを使います |
スタガーに setTimeout を使う | staggerChildren を指定した variants を使います |
AnimatePresence に key がない | 子要素に一意の key を追加します |
| Server Components でアニメーションする | "use client" を追加するか、motion/react-client を使います |
| モーションの軽減を無視する | 常に useReducedMotion() を確認します |
initial を指定せず、マウント時に animate する | 開始状態を定義するために initial を設定します |
レシピ
スクロールに応じたプログレスバー
const { scrollYProgress } = useScroll()
return (
<motion.div
className="fixed top-0 left-0 right-0 h-1 bg-primary origin-left z-50"
style={{ scaleX: scrollYProgress }}
/>
)
アニメーション付きカウンター
const count = useMotionValue(0)
const rounded = useTransform(count, (v) => Math.round(v))
useEffect(() => {
const controls = animate(count, target, { duration: 2 })
return controls.stop
}, [target])
return <motion.span>{rounded}</motion.span>
ページ遷移(Next.js App Router)
// template.tsx
"use client"
import { motion } from "framer-motion"
export default function Template({ children }) {
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
{children}
</motion.div>
)
}
アニメーション付きグラデーションテキスト(シマー効果)
const progress = useMotionValue(0)
useAnimationFrame((time) => {
const duration = 8000
const fullCycle = duration * 2
const cycleTime = (time % fullCycle)
if (cycleTime < duration) {
progress.set((cycleTime / duration) * 100)
} else {
progress.set(100 - ((cycleTime - duration) / duration) * 100)
}
})
const backgroundPosition = useTransform(progress, (p) => `${p}% 50%`)
return (
<motion.span
className="bg-clip-text text-transparent"
style={{
backgroundImage: "linear-gradient(to right, #5227FF, #a78bfa, #c084fc, #5227FF)",
backgroundSize: "300% 100%",
backgroundPosition,
}}
>
{text}
</motion.span>
)