motion-framer

motion-framer

熱門

適用於 React 和 JavaScript 的現代動畫函式庫。透過 motion 元件、variants、手勢(hover/tap/drag)、版面動畫、AnimatePresence 退出動畫、彈簧物理與捲動效果,打造流暢且可上線的動畫。適用於建構互動式 UI 元件、微互動、頁面轉場或複雜動畫序列。

629星標
103分支
更新於 2025/11/20
SKILL.md
readonlyread-only
name
motion-framer
description

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 元件都接受動畫屬性,如 animateinitialtransition,以及手勢屬性如 whileHoverwhileTap 等。

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 結束
/>

資源

官方文件

隨附資源

此技能包含:

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 元件模式

社群資源