Framer Motion 動畫與互動設計套件
Framer Motion 是目前 React 生態系中最強大且易用的動畫函式庫。它提供了宣告式 (Declarative) 的語法,讓你像寫 CSS 一樣簡單地定義動畫,同時支援複雜的手勢互動 (Gestures) 和佈局過渡 (Layout Animations)。
安裝
npm install framer-motion
基本用法 (Basic Animation)
Framer Motion 提供了一系列的 motion 元件(對應 HTML 標籤,如 motion.div、motion.h1)。你可以透過 initial 和 animate 屬性來定義動畫。
import { motion } from 'framer-motion';
export default function MyComponent() {
return (
<motion.div
initial={{ opacity: 0, scale: 0.5 }} // 初始狀態
animate={{ opacity: 1, scale: 1 }} // 動畫結束狀態
transition={{ duration: 0.5 }} // 動畫設定(時間、曲線)
>
Hello Motion!
</motion.div>
);
}
動畫設定 (Transition)
transition 屬性是用來控制動畫「如何」進行的核心。預設情況下,Framer Motion 會根據動畫類型自動選擇最適合的效果(例如:位置移動預設使用物理模擬的 spring,而顏色變化預設使用時間曲線的 tween)。
你也可以手動設定:
<motion.div
animate={{ x: 100 }}
transition={{
type: "spring", // 物理模擬 (預設)
stiffness: 100, // 剛度 (越硬回彈越快)
damping: 10, // 阻尼 (越小回彈次數越多)
mass: 1, // 質量 (越重慣性越大)
}}
/>
<motion.div
animate={{ opacity: 1 }}
transition={{
type: "tween", // 時間曲線
duration: 0.8, // 持續時間 (秒)
ease: "easeInOut", // 缓動函式: "linear", "easeIn", "circOut", "backIn"...
}}
/>
手勢互動 (Gestures)
Framer Motion 內建了強大的手勢偵測,最常用的是 whileHover (滑鼠懸停) 和 whileTap (點擊/按壓)。
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
style={{ padding: '10px 20px' }}
>
按我
</motion.button>
除了 whileHover 和 whileTap,還有其他常用的互動屬性:
- whileFocus: 當元素獲得焦點時 (包含鍵盤 Tab 導航)。
- whileDrag: 當元素正在被拖曳時。
以及對應的事件監聽:
- onHoverStart / onHoverEnd: 滑鼠移入/移出。
- onTap / onTapStart / onTapCancel: 點擊相關事件 (兼容觸控裝置)。
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
onHoverStart={() => console.log('Hover started!')}
>
互動按鈕
</motion.button>
變數 (Variants)
當動畫變得複雜,或者你需要協調父子元件的動畫順序時(例如 Stagger children),使用 variants 是最佳實踐。
- 定義變數物件:將動畫狀態定義為物件。
- 傳遞
variants:將定義好的物件傳給variantsprop。 - 使用字串標籤:在
initial,animate等屬性中只傳入狀態名稱 (key)。
協調 (Orchestration)
Variants 最強大的地方在於父子元件的協調。當父元件的 variant 改變時,會自動流傳 (propagate) 給子元件:
- when: 決定何時執行 (
beforeChildren,afterChildren)。 - staggerChildren: 設定子元件之間的延遲時間 (秒)。
- delayChildren: 設定子元件整體的延遲時間。
const list = {
visible: {
opacity: 1,
transition: {
when: 'beforeChildren', // 在子元件動畫開始前執行
staggerChildren: 0.3, // 每個子元件間隔 0.3 秒顯示
},
},
hidden: {
opacity: 0,
transition: {
when: 'afterChildren',
},
},
};
const item = {
visible: { opacity: 1, x: 0 },
hidden: { opacity: 0, x: -100 },
};
export default function List() {
return (
<motion.ul initial="hidden" animate="visible" variants={list}>
<motion.li variants={item}>Item 1</motion.li>
<motion.li variants={item}>Item 2</motion.li>
<motion.li variants={item}>Item 3</motion.li>
</motion.ul>
);
}
關鍵幀 (Keyframes)
如果你想定義一系列的狀態變化,可以傳入陣列。
<motion.div
animate={{
scale: [1, 2, 2, 1, 1],
rotate: [0, 0, 270, 270, 0],
borderRadius: ['20%', '20%', '50%', '50%', '20%'],
}}
transition={{
duration: 2,
repeat: Infinity, // 無限循環
}}
/>
AnimatePresence (離開動畫)
在 React 中,當元件被 Unmount 時,它會直接從 DOM 消失。如果你想讓它有「離開」的動畫,必須使用 AnimatePresence 包裹它,並定義 exit 屬性。
通常這會搭配 key 屬性來讓 Motion 識別元件的切換。
import { motion, AnimatePresence } from 'framer-motion';
import { useState } from 'react';
export default function Slideshow() {
const [isVisible, setIsVisible] = useState(true);
return (
<>
<button onClick={() => setIsVisible(!isVisible)}>切換</button>
<AnimatePresence>
{isVisible && (
<motion.div
key="modal"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }} // 離開時的動畫
>
我是一個方塊
</motion.div>
)}
</AnimatePresence>
</>
);
}
模式 (Mode)
AnimatePresence 有一個 mode 屬性,預設為 sync (同時執行動畫)。但最常用的是 wait:這會讓當前的元件完全離開後,新元件才開始進入。這對於路由切換或 Tab 切換非常有用。
<AnimatePresence mode="wait">
<motion.div
key={selectedTab ? 'tab1' : 'tab2'}
initial={{ y: 10, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: -10, opacity: 0 }}
transition={{ duration: 0.2 }}
>
{selectedTab ? 'Tab 1 Content' : 'Tab 2 Content'}
</motion.div>
</AnimatePresence>
Layout Animations (自動佈局動畫)
這是 Framer Motion 最神奇的功能之一。只要加上 layout 屬性,當元件的 CSS layout 發生改變(例如寬度改變、Flex 排序改變、或是從一個列表移動到另一個列表)時,它會自動計算並產生平滑的補間動畫。
// 只要加上 layout,切換 flex-direction 時就會有滑順動畫
<motion.div layout style={{ display: 'flex', flexDirection: isOn ? 'row' : 'column' }}>
<motion.div layout>Item 1</motion.div>
<motion.div layout>Item 2</motion.div>
</motion.div>
共享元件轉場 (Shared Element Transitions)
如果你給兩個不同的 motion 元件設定相同的 layoutId,Framer Motion 會在它們之間自動計算位置並產生變形動畫。這就是 App Store 的 "Magic Move" 效果。
常用於:
- 列表點擊後放大成詳情頁。
- Tab 切換時底部的滑動指示器 (Active Indicator)。
function Tabs({ selectedTab, setSelectedTab }) {
const tabs = ['Home', 'About', 'Contact'];
return (
<div className="tabs">
{tabs.map((item) => (
<li
key={item}
onClick={() => setSelectedTab(item)}
style={{ position: 'relative', cursor: 'pointer' }}
>
{item}
{item === selectedTab && (
<motion.div
layoutId="underline" // 關鍵:相同的 ID
style={{
position: 'absolute',
bottom: '-5px',
left: 0,
right: 0,
height: '2px',
background: 'blue',
}}
/>
)}
</li>
))}
</div>
);
}
拖曳互動 (Drag)
只要加上 drag 屬性,元件就可以被拖曳。
<motion.div
drag
dragConstraints={{ left: 0, right: 300, top: 0, bottom: 300 }} // 限制拖曳範圍
dragElastic={0.2} // 拖曳回彈係數 (越小越難拉)
/>
限制在父元件內
通常我們會希望拖曳範圍限制在容器內,這時可以使用 useRef。
import { useRef } from 'react';
import { motion } from 'framer-motion';
function DragWithConstraints() {
const constraintsRef = useRef(null);
return (
<motion.div ref={constraintsRef} style={{ width: 300, height: 300, background: '#eee' }}>
<motion.div
drag
dragConstraints={constraintsRef}
style={{ width: 50, height: 50, background: 'blue' }}
/>
</motion.div>
);
}
捲動動畫 (Scroll Animations)
Framer Motion 的 useScroll Hook 可以讓你取得捲動的進度 (Progress),搭配 useTransform 可以輕鬆做出視差滾動 (Parallax) 或進度條效果。
import { motion, useScroll, useTransform } from 'framer-motion';
function ScrollAnimation() {
const { scrollYProgress } = useScroll();
// 將捲動進度 (0 ~ 1) 轉換為縮放大小 (1 ~ 2)
const scale = useTransform(scrollYProgress, [0, 1], [1, 2]);
return (
<div style={{ height: '200vh' }}>
<motion.div
style={{
scale,
width: 100,
height: 100,
background: 'red',
position: 'fixed',
top: 50,
left: 50,
}}
/>
<h1>請往下滑動</h1>
</div>
);
}
總結
Framer Motion 讓 React 的動畫開發變得非常直觀且強大。從簡單的 Hover 效果到復雜的頁面轉場 (Page Transitions),它都能輕鬆勝任,是提升網站使用者體驗 (UX) 的利器。