F29▸I202▸framer-motion
Card Stack
卡片堆疊
難度 ★★★★★標籤 card · stack · AnimatePresence3 min read
效果簡述
卡片像撲克牌一樣疊著,點下一張時最上面那張往右滑出、後面的往前頂。 Testimonial 輪播、產品特色換頁都好用。
Linear
為設計工程師打造的議題追蹤
Vercel
把 git push 變成上線那一刻
Notion
把腦中的東西攤出來看
Claude Code Prompt
參考:I202 card stack 技術:AnimatePresence + scale/y 階梯 + exit x 飛出 具體行為: - order 陣列存當前順序,next() 把第一個推到尾巴 - map order.slice(0, 3):i=0 是最前,i=1/2 是後面 - scale: 1 - i * 0.06,y: i * -10(後面的往上偏一點) - zIndex: 10 - i - exit: x: 240, rotate: 8(往右飛) 限制: - 只渲染前 3 張就夠(後面的看不到) - 卡片寬度固定,不然飛出時看起來很怪 - 點擊事件要綁在最頂那張,後面的 pointer-events: none
完整程式碼
'use client'
import { AnimatePresence, motion } from 'framer-motion'
import { useState } from 'react'
const cards = [
{ id: 1, title: 'Linear', text: '為設計工程師打造' },
{ id: 2, title: 'Vercel', text: 'git push 變上線' },
// ...
]
export function CardStack() {
const [order, setOrder] = useState(cards)
function next() { setOrder((p) => [...p.slice(1), p[0]]) }
return (
<>
<div className="relative h-48 w-72">
<AnimatePresence>
{order.slice(0, 3).map((c, i) => (
<motion.div
key={c.id}
className="absolute inset-0 rounded-xl border bg-card p-5"
initial={{ scale: 1 - i * 0.06, y: i * -10, opacity: 0 }}
animate={{ scale: 1 - i * 0.06, y: i * -10, opacity: 1, zIndex: 10 - i }}
exit={{ x: 240, opacity: 0, rotate: 8 }}
transition={{ duration: 0.4 }}
>
{c.title}
</motion.div>
))}
</AnimatePresence>
</div>
<button onClick={next}>下一張</button>
</>
)
}何時用
- ✅ Testimonial 輪播
- ✅ 產品特色 walkthrough
- ✅ 主視覺多面向展示
何時別用
- ❌ 卡片數量 < 3(看不出 stack)
- ❌ 內容需要被同時看見(用 grid 就好)
- ❌ SEO 重要的內容(後面卡片在 DOM 但視覺被擋)
我踩過的坑
-
沒固定寬高 exit 動畫變形:absolute inset-0 + 父層固定尺寸 才能讓飛出動畫漂亮。
-
map 整個陣列效能差:只渲前 3 張,剩下根本看不到。
-
zIndex 漏掉互動會錯亂:最上面的卡 zIndex 必須最高,不然點到 底下那張。
相關效果
- F09 Tilt 3D Card — 單卡 hover
- F28 Image Compare — 另一種互動切換