Modern animation library for React and JavaScript. Create smooth, production-ready animations with motion components, variants, gestures (hover/tap/drag), layout animations, AnimatePresence exit animations, spring physics, and scroll-based effects. Use when building interactive UI components, micro-interactions, page transitions, or complex animation sequences.
Motion 與 Framer Motion
總覽
Motion(前身為 Framer Motion)是一個適用於 React 與 JavaScript 的生產級動畫函式庫,能以最少的程式碼實現宣告式且高效能的動畫。它提供 motion 元件,為 HTML 元素加上動畫超能力,支援手勢辨識(hover、tap、drag、focus),並包含版面動畫、退出動畫與彈簧物理等進階功能。
何時使用此技能:
- 建構互動式 UI 元件(按鈕、卡片、選單)
- 建立微互動與 hover 效果
- 實作頁面轉場與路由動畫
- 加入捲動動畫與視差效果
- 動畫化版面變更(縮放、排序、共享元素轉場)
- 拖放介面
- 複雜動畫序列與狀態式動畫
- 取代 CSS transition,改用更強大、可控的動畫
技術:
- Motion (v11+) - 來自 Framer Motion 團隊的現代化、更輕量的函式庫
- Framer Motion - 功能完整的前代版本(仍廣泛使用)
- 相容 React 18+,也支援 Vue
- 支援 TypeScript
- 可搭配 Next.js、Vite、Remix 及所有現代 React 框架
核心概念
1. Motion 元件
將任何 HTML/SVG 元素加上 motion. 前綴,即可轉換為可動畫的元件:
import { motion } from "framer-motion"
// 一般 HTML 變成 motion 元件
<motion.div />
<motion.button />
<motion.svg />
<motion.path />
每個 motion 元件都接受動畫屬性,如 animate、initial、transition,以及手勢屬性如 whileHover、whileTap 等。
2. Animate 屬性
animate 屬性定義目標動畫狀態。當數值改變時,Motion 會自動動畫到該狀態:
// 簡單動畫 - x 位置改變
<motion.div animate={{ x: 100 }} />
// 多個屬性
<motion.div animate={{ x: 100, opacity: 1, scale: 1.2 }} />
// 狀態改變時觸發動畫
const [isOpen, setIsOpen] = useState(false)
<motion.div animate={{ width: isOpen ? 300 : 100 }} />
3. 初始狀態
使用 initial 屬性設定動畫前的初始狀態:
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
/>
設定 initial={false} 可停用掛載時的初始動畫。
4. 轉場(Transitions)
使用 transition 屬性控制動畫在狀態之間的移動方式:
// 基於時間長度
<motion.div
animate={{ x: 100 }}
transition={{ duration: 0.5, ease: "easeInOut" }}
/>
// 彈簧物理
<motion.div
animate={{ scale: 1.2 }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
/>
// 不同屬性使用不同轉場
<motion.div
animate={{ x: 100, opacity: 1 }}
transition={{
x: { type: "spring", stiffness: 300 },
opacity: { duration: 0.2 }
}}
/>
轉場類型:
"tween"(預設)- 基於時間長度並搭配 easing"spring"- 基於物理的彈簧動畫"inertia"- 減速動畫(用於拖曳)
5. Variants
使用具名 variants 組織動畫狀態,讓程式碼更簡潔,並可傳播至子元件:
const variants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
exit: { opacity: 0, scale: 0.9 }
}
<motion.div
variants={variants}
initial="hidden"
animate="visible"
exit="exit"
/>
Variant 傳播 - 子元件會自動繼承父元件的 variant 狀態:
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1 // 錯開子動畫
}
}
}
const itemVariants = {
hidden: { x: -20, opacity: 0 },
visible: { x: 0, opacity: 1 }
}
<motion.ul variants={containerVariants} initial="hidden" animate="visible">
<motion.li variants={itemVariants} />
<motion.li variants={itemVariants} />
<motion.li variants={itemVariants} />
</motion.ul>
常見模式
1. Hover 動畫
使用 whileHover 屬性在 hover 時觸發動畫:
// 簡單 hover 效果
<motion.button
whileHover={{ scale: 1.1 }}
transition={{ duration: 0.2 }}
>
Hover me
</motion.button>
// 多個屬性
<motion.div
whileHover={{
scale: 1.05,
backgroundColor: "#f0f0f0",
boxShadow: "0px 10px 30px rgba(0, 0, 0, 0.2)"
}}
>
Hover card
</motion.div>
// 搭配自訂轉場
<motion.button
whileHover={{
scale: 1.2,
transition: { duration: 0.1 } // 手勢開始時的轉場
}}
transition={{ duration: 0.5 }} // 手勢結束時的轉場
>
Button
</motion.button>
巢狀元素的 Hover:
<motion.div whileHover="hover" variants={cardVariants}>
<motion.h3 variants={titleVariants}>Title</motion.h3>
<motion.img variants={imageVariants} />
</motion.div>
2. Tap/按壓動畫
使用 whileTap 屬性在點擊/按壓時觸發動畫:
// 點擊時縮小
<motion.button
whileTap={{ scale: 0.9 }}
>
Click me
</motion.button>
// 結合 hover 與 tap
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95, rotate: 3 }}
>
Interactive button
</motion.button>
// 搭配 variants
const buttonVariants = {
rest: { scale: 1 },
hover: { scale: 1.1 },
pressed: { scale: 0.95 }
}
<motion.button
variants={buttonVariants}
initial="rest"
whileHover="hover"
whileTap="pressed"
>
Button
</motion.button>
3. 拖曳互動
使用 drag 屬性讓元素可拖曳:
// 基本拖曳(雙軸)
<motion.div drag />
// 限制軸向
<motion.div drag="x" /> // 僅水平
<motion.div drag="y" /> // 僅垂直
// 拖曳限制
<motion.div
drag
dragConstraints={{ left: -100, right: 100, top: -100, bottom: 100 }}
/>
// 以父元素作為限制
<motion.div ref={constraintsRef}>
<motion.div drag dragConstraints={constraintsRef} />
</motion.div>
// 拖曳時的視覺回饋
<motion.div
drag
whileDrag={{
scale: 1.1,
boxShadow: "0px 10px 20px rgba(0,0,0,0.2)",
cursor: "grabbing"
}}
dragElastic={0.1} // 拖出限制時的彈性
dragTransition={{ bounceStiffness: 600, bounceDamping: 20 }}
/>
拖曳事件:
<motion.div
drag
onDragStart={(event, info) => console.log(info.point)}
onDrag={(event, info) => console.log(info.offset)}
onDragEnd={(event, info) => console.log(info.velocity)}
/>
4. 退出動畫(AnimatePresence)
使用 AnimatePresence 在元件從 DOM 移除時播放動畫:
import { AnimatePresence } from "framer-motion"
// 基本退出動畫
<AnimatePresence>
{isVisible && (
<motion.div
key="modal"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
/>
)}
</AnimatePresence>
必要條件:
- 元件必須是
<AnimatePresence>的直接子元素 - 必須有唯一的
key屬性 - 使用
exit屬性定義退出動畫
列表項目的退出動畫:
<AnimatePresence>
{items.map(item => (
<motion.li
key={item.id}
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 50 }}
layout // 平滑的版面位移
>
{item.name}
</motion.li>
))}
</AnimatePresence>
錯開的退出動畫:
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
when: "beforeChildren",
staggerChildren: 0.1
}
},
exit: {
opacity: 0,
transition: {
when: "afterChildren",
staggerChildren: 0.05,
staggerDirection: -1 // 反向順序
}
}
}
<AnimatePresence>
{show && (
<motion.div variants={containerVariants} initial="hidden" animate="visible" exit="exit">
<motion.div variants={itemVariants} />
<motion.div variants={itemVariants} />
<motion.div variants={itemVariants} />
</motion.div>
)}
</AnimatePresence>
5. 版面動畫
使用 layout 屬性自動動畫化版面變更(位置、大小):
// 動畫化所有版面變更
<motion.div layout />
// 僅動畫化位置變更
<motion.div layout="position" />
// 僅動畫化大小變更
<motion.div layout="size" />
網格版面動畫:
const [columns, setColumns] = useState(3)
<motion.div className="grid">
{items.map(item => (
<motion.div
key={item.id}
layout
transition={{ layout: { duration: 0.3, ease: "easeInOut" } }}
/>
))}
</motion.div>
共享版面動畫(layoutId):
使用 layoutId 連接兩個不同元素,實現平滑轉場:
// 頁籤指示器範例
<nav>
{tabs.map(tab => (
<button key={tab.id} onClick={() => setActive(tab.id)}>
{tab.label}
{activeTab === tab.id && (
<motion.div
layoutId="underline"
style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 2 }}
/>
)}
</button>
))}
</nav>
// 從縮圖開啟的 Modal
<motion.img
src={thumbnail}
layoutId="product-image"
onClick={() => setExpanded(true)}
/>
<AnimatePresence>
{expanded && (
<motion.div layoutId="product-image">
<img src={fullsize} />
</motion.div>
)}
</AnimatePresence>
6. 捲動動畫
使用 whileInView 在元素進入視埠時觸發動畫:
<motion.div
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.8 }} // once: 僅觸發一次,amount: 80% 可見
transition={{ duration: 0.5 }}
>
Animates when scrolled into view
</motion.div>
Viewport 選項:
once: true- 動畫僅觸發一次amount: 0.5- 元素可見百分比(0-1)或 "some" | "all"margin: "-100px"- 偏移視埠邊界
錯開的捲動動畫:
<motion.ul
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.3 }}
variants={{
visible: {
opacity: 1,
transition: { staggerChildren: 0.1 }
},
hidden: { opacity: 0 }
}}
>
<motion.li variants={itemVariants} />
<motion.li variants={itemVariants} />
<motion.li variants={itemVariants} />
</motion.ul>
7. 彈簧動畫
使用彈簧物理實現自然、有彈性的動畫:
// 基本彈簧
<motion.div
animate={{ scale: 1.2 }}
transition={{ type: "spring" }}
/>
// 自訂彈簧物理
<motion.div
animate={{ x: 100 }}
transition={{
type: "spring",
stiffness: 300, // 數值越高越快、越靈敏(預設:100)
damping: 20, // 數值越高越不彈(預設:10)
mass: 1, // 數值越高慣性越大(預設:1)
}}
/>
// 視覺時間長度(較易控制彈簧)
<motion.div
animate={{ rotate: 90 }}
transition={{
type: "spring",
visualDuration: 0.5, // 感知時間長度
bounce: 0.25 // 彈性(0-1,預設:0.25)
}}
/>
彈簧預設:
- 柔和:
stiffness: 100, damping: 20 - 搖晃:
stiffness: 200, damping: 10 - 堅硬:
stiffness: 400, damping: 30 - 緩慢:
stiffness: 50, damping: 20
手勢辨識
Motion 提供宣告式的手勢處理器:
手勢屬性
<motion.div
whileHover={{ scale: 1.1 }} // 指標懸停在元素上
whileTap={{ scale: 0.9 }} // 主要指標按壓元素
whileFocus={{ outline: "2px" }} // 元素取得焦點
whileDrag={{ scale: 1.1 }} // 元素正在被拖曳
whileInView={{ opacity: 1 }} // 元素在視埠內
/>
手勢事件
<motion.div
onHoverStart={(event, info) => {}}
onHoverEnd={(event, info) => {}}
onTap={(event, info) => {}}
onTapStart={(event, info) => {}}
onTapCancel={(event, info) => {}}
onDragStart={(event, info) => {}}
onDrag={(event, info) => {}}
onDragEnd={(event, info) => {}}
onViewportEnter={(entry) => {}}
onViewportLeave={(entry) => {}}
/>
事件資訊物件包含:
point: { x, y }- 頁面座標offset: { x, y }- 從拖曳開始的偏移量velocity: { x, y }- 拖曳速度
Hooks
useAnimate
使用 useAnimate hook 手動控制動畫:
import { useAnimate } from "framer-motion"
function Component() {
const [scope, animate] = useAnimate()
useEffect(() => {
// 動畫化多個元素
animate([
[scope.current, { opacity: 1 }],
["li", { x: 0, opacity: 1 }, { delay: stagger(0.1) }],
[".button", { scale: 1.2 }]
])
}, [])
return (
<div ref={scope}>
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
<button className="button">Click</button>
</div>
)
}
動畫控制:
const controls = animate(element, { x: 100 })
controls.play()
controls.pause()
controls.stop()
controls.speed = 0.5
controls.time = 0 // 跳到開始
useSpring
建立彈簧動畫的 motion 數值:
import { useSpring } from "framer-motion"
function Component() {
const x = useSpring(0, { stiffness: 300, damping: 20 })
return (
<motion.div style={{ x }}>
<button onClick={() => x.set(100)}>Move</button>
</motion.div>
)
}
useInView
偵測元素是否在視埠內:
import { useInView } from "framer-motion"
function Component() {
const ref = useRef(null)
const isInView = useInView(ref, { once: true, amount: 0.5 })
return (
<div ref={ref}>
{isInView ? "In view!" : "Not in view"}
</div>
)
}
整合模式
與 GSAP 整合
結合 Motion 處理 React 狀態式動畫,GSAP 處理複雜時間軸:
import { motion } from "framer-motion"
import gsap from "gsap"
function Component() {
const boxRef = useRef()
const handleClick = () => {
// 使用 GSAP 處理複雜時間軸
const tl = gsap.timeline()
tl.to(boxRef.current, { rotation: 360, duration: 1 })
.to(boxRef.current, { scale: 1.5, duration: 0.5 })
}
return (
// 使用 Motion 處理 hover/tap/layout 動畫
<motion.div
ref={boxRef}
whileHover={{ scale: 1.1 }}
onClick={handleClick}
/>
)
}
與 React Three Fiber 整合
使用 Motion 數值動畫化 3D 物件:
import { motion } from "framer-motion"
import { useFrame } from "@react-three/fiber"
function Box() {
const x = useMotionValue(0)
useFrame(() => {
// 將 Motion 數值同步到 Three.js 位置
meshRef.current.position.x = x.get()
})
return (
<>
<mesh ref={meshRef}>
<boxGeometry />
<meshStandardMaterial />
</mesh>
<motion.div
style={{ x }}
drag="x"
dragConstraints={{ left: -5, right: 5 }}
/>
</>
)
}
與表單函式庫整合
動畫化表單驗證狀態:
import { motion, AnimatePresence } from "framer-motion"
function FormField({ error }) {
return (
<div>
<motion.input
animate={{
borderColor: error ? "#ff0000" : "#cccccc",
x: error ? [0, -10, 10, -10, 10, 0] : 0 // 搖晃動畫
}}
transition={{ duration: 0.4 }}
/>
<AnimatePresence>
{error && (
<motion.p
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
style={{ color: "#ff0000" }}
>
{error}
</motion.p>
)}
</AnimatePresence>
</div>
)
}
效能最佳化
1. 使用 Transform 屬性
Transform 屬性(x、y、scale、rotate)具備硬體加速:
// ✅ 良好 - 硬體加速
<motion.div animate={{ x: 100, scale: 1.2 }} />
// ❌ 避免 - 觸發 layout/paint
<motion.div animate={{ left: 100, width: 200 }} />
2. 個別 Transform 屬性
Motion 支援個別 transform 屬性,讓程式碼更簡潔:
// 個別屬性(Motion 功能)
<motion.div style={{ x: 100, rotate: 45, scale: 1.2 }} />
// 傳統方式(也支援)
<motion.div style={{ transform: "translateX(100px) rotate(45deg) scale(1.2)" }} />
3. 減少動畫以提升無障礙性
尊重使用者對減少動畫的偏好:
import { useReducedMotion } from "framer-motion"
function Component() {
const shouldReduceMotion = useReducedMotion()
return (
<motion.div
animate={{ x: 100 }}
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.5 }}
/>
)
}
4. 版面動畫效能
版面動畫可能較耗效能。最佳化方式:
// 指定要動畫的內容
<motion.div layout="position" /> // 僅位置,不含大小
// 最佳化轉場
<motion.div
layout
transition={{
layout: { duration: 0.3, ease: "easeOut" }
}}
/>
5. 謹慎使用 layoutId
layoutId 會建立共享版面動畫,但會全域追蹤元素。僅在需要時使用。
常見陷阱
1. 忘記為退出動畫加上 AnimatePresence
問題: 退出動畫無法運作
// ❌ 錯誤 - 沒有 AnimatePresence
{show && <motion.div exit={{ opacity: 0 }} />}
// ✅ 正確 - 包在 AnimatePresence 內
<AnimatePresence>
{show && <motion.div exit={{ opacity: 0 }} />}
</AnimatePresence>
2. 列表中缺少 key 屬性
問題: AnimatePresence 無法追蹤元素
// ❌ 錯誤 - 沒有 key
<AnimatePresence>
{items.map(item => <motion.div exit={{ opacity: 0 }} />)}
</AnimatePresence>
// ✅ 正確 - 唯一 keys
<AnimatePresence>
{items.map(item => (
<motion.div key={item.id} exit={{ opacity: 0 }} />
))}
</AnimatePresence>
3. 動畫非 Transform 屬性
問題: 動畫卡頓、效能不佳
// ❌ 避免 - 非硬體加速
<motion.div animate={{ top: 100, left: 50, width: 200 }} />
// ✅ 較佳 - 使用 transforms
<motion.div animate={{ x: 50, y: 100, scaleX: 2 }} />
4. 過度使用版面動畫
問題: 大量版面動畫元素導致效能問題
// ❌ 太多版面動畫
{items.map(item => <motion.div layout>{item}</motion.div>)}
// ✅ 僅在需要處使用 layout,其他最佳化
{items.map(item => (
<motion.div
key={item.id}
animate={{ opacity: 1 }} // 較便宜的動畫
exit={{ opacity: 0 }}
/>
))}
5. 複雜動畫未使用 Variants
問題: 動畫程式碼重複、無法協調子元素
// ❌ 重複
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} />
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} />
// ✅ 使用 variants
const variants = {
hidden: { opacity: 0 },
visible: { opacity: 1 }
}
<motion.div variants={variants} initial="hidden" animate="visible" />
<motion.div variants={variants} initial="hidden" animate="visible" />
6. 轉場時間設定錯誤
問題: 轉場未套用至特定手勢
// ❌ 錯誤 - 一般轉場不會套用至 whileHover
<motion.div
whileHover={{ scale: 1.2 }}
transition={{ duration: 1 }} // 這套用於 animate 屬性,而非 whileHover
/>
// ✅ 正確 - 在 whileHover 中設定轉場,或使用獨立的手勢轉場
<motion.div
whileHover={{
scale: 1.2,
transition: { duration: 0.2 } // 套用於 hover 開始
}}
transition={{ duration: 0.5 }} // 套用於 hover 結束
/>
資源
官方文件
- Motion Docs - 官方 Motion 文件
- Framer Motion Docs - Framer Motion(舊版)
- Motion GitHub - 原始碼與範例
隨附資源
此技能包含:
references/
api_reference.md- 完整的 Motion API 參考variants_patterns.md- Variant 模式與協調gesture_guide.md- 全面的手勢處理指南
scripts/
animation_generator.py- 產生 Motion 元件樣板variant_builder.py- 互動式 variant 設定工具
assets/
starter_motion/- 完整的 Motion + Vite 起始範本examples/- 真實世界的 Motion 元件模式
社群資源
- Motion Dev Discord - 官方社群
- Framer Motion Examples - 互動範例
- Motion Recipes - 常見模式
- CodeSandbox Templates - 線上示範






