Framer Motion 動畫與互動設計套件

Framer Motion 是目前 React 生態系中最強大且易用的動畫函式庫。它提供了宣告式 (Declarative) 的語法,讓你像寫 CSS 一樣簡單地定義動畫,同時支援複雜的手勢互動 (Gestures) 和佈局過渡 (Layout Animations)。

安裝

npm install framer-motion

基本用法 (Basic Animation)

Framer Motion 提供了一系列的 motion 元件(對應 HTML 標籤,如 motion.divmotion.h1)。你可以透過 initialanimate 屬性來定義動畫。

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>

除了 whileHoverwhileTap,還有其他常用的互動屬性:

  • 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 是最佳實踐。

  1. 定義變數物件:將動畫狀態定義為物件。
  2. 傳遞 variants:將定義好的物件傳給 variants prop。
  3. 使用字串標籤:在 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) 的利器。