個人網頁開發
EP.07

Framer Motion
讓頁面元素動起來

我的網頁所有滑入、淡出、滾動觸發動畫都靠它,
幾行程式碼就能讓靜態頁面瞬間有生命感

Joseph Chen

2024
5 min read
1.2k views

你有沒有注意到,打開我的個人網頁時,頁面上的元素不是「突然出現」,而是從下方滑入、逐漸淡出?滾動到某個區塊時,卡片一張一張延遲出現?

這些全都來自 Framer Motion。它的設計哲學很簡單:把動畫寫成 props,不是 CSS keyframes

你已經在我的網頁裡看過無數次的這段
<motion.div
  initial={{ opacity: 0, y: 20 }}   // 從這個狀態開始
  animate={{ opacity: 1, y: 0 }}    // 動畫到這個狀態
  transition={{ duration: 0.6 }}    // 花 0.6>
  <h1>Joseph Chen</h1>
</motion.div>

這篇帶你從基礎到實際用法,全部都是我的網頁裡真實出現的動畫。

1. motion 元件 — 最基本的概念

Framer Motion 的用法是把原本的 HTML 元素換成 motion. 版本:

bash
// 原本
<div>...</div>
<h1>...</h1>
<section>...</section>

// 加上動畫能力
<motion.div>...</motion.div>
<motion.h1>...</motion.h1>
<motion.section>...</motion.section>

換成 motion.div 之後,原本 div 的所有行為都保留,只是多了動畫 props 可以用。

💡
記得 import! 使用前需要引入:import { motion } from 'framer-motion';

2. initial + animate — 進場動畫

initial 是元件出現前的狀態,animate 是最終狀態。Framer Motion 自動補間兩者之間的動畫。

淡入(fade in)

bash
<motion.div
  initial={{ opacity: 0 }}
  animate={{ opacity: 1 }}
  transition={{ duration: 0.5 }}
>

效果預覽(重新整理頁面可看到)

淡入淡出

從下方滑入(最常用)

我的首頁 Hero 區塊用的動畫
<motion.div
  initial={{ opacity: 0, y: 20 }}  // 透明 + 往下 20px
  animate={{ opacity: 1, y: 0 }}   // 不透明 + 回到原位
  transition={{ duration: 0.6 }}
>

效果預覽(可點「重播」)

從下方滑入

從左方滑入

我的首頁文字區塊用的動畫
<motion.div
  initial={{ opacity: 0, x: -30 }}  // 往左 30px
  animate={{ opacity: 1, x: 0 }}
  transition={{ duration: 0.8 }}
>
常用屬性說明典型值
opacity透明度0 → 1
y垂直位移(正 = 往下)20 → 0 或 -20 → 0
x水平位移(正 = 往右)-30 → 0 或 30 → 0
scale縮放0.8 → 1
rotate旋轉角度90 → 0

3. transition — 控制動畫節奏

transition 控制動畫的時間、緩動和延遲:

transition 完整範例
<motion.div
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{
    duration: 0.6,    // 持續時間(秒)
    delay: 0.2,       // 延遲幾秒後開始
    ease: "easeOut",  // 緩動函式:開始快、結尾慢(最自然)
  }}
>

delay 的妙用:讓元素「逐一出現」

我的 blog/page.tsx 裡的每個系列卡片,用 index × 0.1 製造錯開效果,讓卡片一張一張依序出現,而不是全部同時跳出:

blog/page.tsx — 逐一出現效果
{series.map((s, i) => (
  <motion.section
    key={s.id}
    initial={{ opacity: 0, y: 24 }}
    whileInView={{ opacity: 1, y: 0 }}
    transition={{
      duration: 0.5,
      delay: i * 0.1,  // i=00s, i=10.1s, i=20.2s
    }}
  >
    <SeriesSection s={s} />
  </motion.section>
))}

4. whileInView — 滾動觸發動畫

animate 是頁面載入時立刻播放。但我更常用 whileInView等到元素滾動進入視窗時才觸發動畫

這讓使用者滾動到任何區塊都有「內容飛入」的感覺,而不是一開始全部動完之後就靜止了。

whileInView 基本用法
<motion.div
  initial={{ opacity: 0, y: 24 }}     // 初始:透明 + 往下
  whileInView={{ opacity: 1, y: 0 }}  // 滾動到視窗內:動畫到這
  viewport={{ once: true }}           // once: true = 只觸發一次,不要每次滾回去都重播
  transition={{ duration: 0.5 }}
>
viewport={ once: true } 非常重要!
  • once: true — 滾動到時播放一次,往上捲再回來不重播(推薦)
  • once: false(預設) — 每次進出視窗都重播,通常太吵

我的每一篇文章頁面都大量使用 whileInView,讓讀者越往下滾、內容越活躍:

文章頁面的區塊動畫
// 每個 section 滾動進來時淡入
<motion.section
  initial={{ opacity: 0, y: 20 }}
  whileInView={{ opacity: 1, y: 0 }}
  viewport={{ once: true }}
  transition={{ duration: 0.5 }}
  className="space-y-6"
>
  <h2>這個章節的標題</h2>
  ...
</motion.section>

5. whileHover / whileTap — 互動反饋

除了進場動畫,Framer Motion 還能處理 hover 和點擊的微互動,讓按鈕、卡片有「按下去」的手感:

bash
<motion.button
  whileHover={{ scale: 1.05 }}   // hover 時放大 5%
  whileTap={{ scale: 0.95 }}     // 點擊時縮小 5%(按壓感)
  transition={{ type: "spring", stiffness: 400, damping: 17 }}
>
  點我
</motion.button>

試試看 — 滑鼠移上去、點一下

💡
type: "spring" 是彈簧緩動,比一般 easeOut 更有彈性、更自然。stiffness(彈簧硬度)越高越快,damping(阻尼)越高越少回彈。stiffness: 400, damping: 17 是我最喜歡的組合,快速且稍微回彈一下。

6. AnimatePresence — 離場動畫

正常的 React 元件消失時是瞬間消失的。AnimatePresence 讓元件可以有「離場動畫」,等動畫播完才真正從 DOM 移除:

bash
import { AnimatePresence, motion } from 'framer-motion';

function ToggleBox({ show }) {
  return (
    <AnimatePresence>
      {show && (
        <motion.div
          initial={{ opacity: 0, height: 0 }}
          animate={{ opacity: 1, height: 'auto' }}
          exit={{ opacity: 0, height: 0 }}    // ← 離場時的狀態
          transition={{ duration: 0.3 }}
        >
          這個內容會平滑收起
        </motion.div>
      )}
    </AnimatePresence>
  );
}

展開/收起 demo

這段內容會平滑展開和收起,不是瞬間跳動。

完整案例:我的首頁動畫架構

把以上學到的全部組合起來,這是我的首頁 Hero 區塊的完整動畫寫法:

app/page.tsx — Hero 區塊(動畫部分)
'use client';
import { motion } from 'framer-motion';

export default function Home() {
  return (
    <section className="pt-20 pb-32">
      {/* 文字區塊:從左滑入 */}
      <motion.div
        initial={{ opacity: 0, x: -30 }}
        animate={{ opacity: 1, x: 0 }}
        transition={{ duration: 0.8 }}
      >
        <h1>I think, therefore I am</h1>
        <Button>View Resume</Button>
      </motion.div>

      {/* 圖片區塊:從右滑入,延遲 0.2 秒 */}
      <motion.div
        initial={{ opacity: 0, x: 30 }}
        animate={{ opacity: 1, x: 0 }}
        transition={{ duration: 0.8, delay: 0.2 }}
      >
        <Image src="/profile.png" />
      </motion.div>

      {/* 技術標籤:從下方滑入,再延遲 0.4 秒 */}
      <motion.div
        initial={{ opacity: 0, y: 20 }}
        animate={{ opacity: 1, y: 0 }}
        transition={{ duration: 0.6, delay: 0.4 }}
      >
        {/* 技術 Chip 們 */}
      </motion.div>
    </section>
  );
}
文字和圖片錯開進場是我網頁最核心的動畫效果。文字從左、圖片從右、標籤從下,三個方向加上遞增 delay,製造「內容逐步建立」的層次感。

這篇學到什麼

🎬motion.div = 原本的 div + 動畫能力,所有 HTML 元素都有對應的 motion.xxx 版本
▶️initial(起始狀態)+ animate(目標狀態)= 頁面載入時播放的進場動畫
⏱️transition 控制 duration(時間)、delay(延遲)、ease(緩動)
📜whileInView + viewport={{once:true}} = 滾動到時才觸發,只觸發一次
🖱️whileHover + whileTap + spring = 有彈性的按壓互動感
AnimatePresence + exit = 元件消失時有離場動畫,不再瞬間消失
Web Dev
Framer Motion
動畫
whileInView
EP.07