Frontend Lab
← 回到所有效果
F29I202framer-motion

Card Stack

卡片堆疊

難度 ★★★★★標籤 card · stack · AnimatePresence3 min read

效果簡述

卡片像撲克牌一樣疊著,點下一張時最上面那張往右滑出、後面的往前頂。 Testimonial 輪播、產品特色換頁都好用。

Linear
為設計工程師打造的議題追蹤
Vercel
把 git push 變成上線那一刻
Notion
把腦中的東西攤出來看
預設|4 張卡, 右飛離場

Claude Code Prompt

💬 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

完整程式碼

CardStack.tsx
'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 但視覺被擋)

我踩過的坑

  1. 沒固定寬高 exit 動畫變形:absolute inset-0 + 父層固定尺寸 才能讓飛出動畫漂亮。

  2. map 整個陣列效能差:只渲前 3 張,剩下根本看不到。

  3. zIndex 漏掉互動會錯亂:最上面的卡 zIndex 必須最高,不然點到 底下那張。

相關效果

搜尋

按 ⌘K 隨時開啟