Swipe Cards
Tinder 式滑卡堆疊,可拖曳、依速度飛出,並提供程式化左右滑與空狀態。
把這個效果裝進你的專案
在專案資料夾執行指令,CLI 會加入原始碼與依賴套件,再依下方使用範例引用元件。
npx webberui@latest add swipe-cardsSwipe Cards 是用 React、Tailwind CSS 與 Motion 做的 Tinder 式滑卡(swipe cards)堆疊元件:最上層卡片可以用滑鼠或觸控左右拖曳,位移超過閾值或甩動夠快就飛出,另外也能透過 ref 與方向鍵操作。
Playground
即時調整 props、程式碼片段同步更新——直接試出你要的樣子再複製。
<SwipeCards/>
完整範例包含預覽的排版與資料。先安裝元件,再把範例檔貼入你的專案。
適用情境
- 新手引導時讓使用者快速挑選興趣主題,作為之後推薦內容的依據。
- 求職、租屋、配對這類逐筆瀏覽並決定「要/不要」的流程。
- 內容審核或標註工具,一次處理一張卡片的通過與退回。
- 產品回饋或功能投票,讓使用者對候選功能逐一表態。
- 不適合:清單列上的左滑刪除、封存等動作,改用 Swipe Action List。
安裝
npx webberui@latest add https://webberui.com/r/swipe-cards.json或在 components.json 設定 registries 後,改用 @webberui/swipe-cards 安裝。
使用
import * as React from "react";
import {
SwipeCards,
type SwipeCardsHandle,
} from "@/components/ui/swipe-cards";
const ref = React.useRef<SwipeCardsHandle>(null);
<SwipeCards
ref={ref}
cards={[
{ id: 1, content: <div className="h-52 rounded-2xl bg-sky-500" /> },
{ id: 2, content: <div className="h-52 rounded-2xl bg-rose-500" /> },
]}
onSwipe={(id, dir) => console.log(id, dir)}
className="h-60 w-72"
/>;
// 程式化左右滑
<button onClick={() => ref.current?.swipe("left")}>略過</button>
<button onClick={() => ref.current?.swipe("right")}>喜歡</button>最上層卡片可用滑鼠或觸控往左右拖曳,x 位移會即時映射成旋轉角度與「喜歡/略過」提示;放開時若位移超過 threshold 或甩動速度超過 velocityThreshold,卡片會朝該方向飛出並露出下一張。全部滑完後顯示 emptyState。
範例
收集使用者偏好
把 cards 定義在元件外面或用 useMemo,再用 onSwipe 記錄右滑的項目,全部滑完時在 emptyState 顯示結果。
import * as React from "react";
import { SwipeCards, type SwipeDirection } from "@/components/ui/swipe-cards";
const topics = [
{ id: "design", title: "介面設計" },
{ id: "frontend", title: "前端開發" },
{ id: "data", title: "資料分析" },
{ id: "pm", title: "產品管理" },
];
const cards = topics.map((t) => ({
id: t.id,
content: (
<div className="flex h-64 w-64 items-center justify-center rounded-2xl border bg-white text-lg font-medium shadow-sm dark:bg-neutral-900">
{t.title}
</div>
),
}));
export function TopicPicker() {
const [liked, setLiked] = React.useState<string[]>([]);
return (
<SwipeCards
cards={cards}
className="h-80"
onSwipe={(id, dir: SwipeDirection) => {
if (dir === "right") setLiked((prev) => [...prev, String(id)]);
}}
emptyState={<p className="text-sm">已選擇 {liked.length} 個主題</p>}
/>
);
}重新開始一輪
元件在 cards 陣列參照改變時把進度歸零,所以換一個新陣列就能重新開始,也能用來載入下一批資料。
const [deck, setDeck] = React.useState(() => createCards(listings));
<SwipeCards
cards={deck}
className="h-96"
emptyState={<button onClick={() => setDeck(createCards(listings))}>重新瀏覽</button>}
/>調整滑動靈敏度
降低 threshold 與 velocityThreshold,小幅拖曳或輕甩就能讓卡片飛出,適合卡片寬度較窄的手機版面。
<SwipeCards cards={cards} threshold={60} velocityThreshold={300} className="h-72" />Props
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
cards | { id: string | number; content: ReactNode }[] | — | 卡片陣列,第一項在最上層 |
onSwipe | (id, dir: "left" | "right") => void | — | 滑掉一張卡時觸發 |
emptyState | ReactNode | 預設提示 | 全部滑完後顯示的內容 |
threshold | number | 100 | 觸發飛出的水平位移閾值(px) |
velocityThreshold | number | 500 | 觸發飛出的甩動速度閾值(px/s) |
className | string | — | 堆疊容器樣式(通常在此指定高度) |
Ref(SwipeCardsHandle)
| 方法 | 型別 | 說明 |
|---|---|---|
swipe | (dir: "left" | "right") => void | 以程式方式滑掉最上層卡片,可用於自訂按鈕 |
細節
- 後方卡片依深度逐層縮小並下移,露出邊緣形成堆疊感;同時最多渲染 3 層。
- 拖曳採
drag="x",useTransform將位移映射為旋轉角度;飛出使用 spring,離場後才更新索引並讓下一張補位。 - 容器高度建議透過
className指定(例如h-60),卡片內容自行決定寬高;容器不裁切,飛出的卡片才能完整離場。
可及性
- 最上層卡片可聚焦(
tabIndex=0),支援 → 喜歡、← 略過,並帶有role="group"與操作說明的aria-label。 - 後方卡片以
aria-hidden對輔助科技隱藏,且pointer-events: none,只有最上層可互動。 - 使用者系統開啟「減少動態效果」時,停用拖曳與旋轉,改以按鈕/鍵盤觸發淡出切換。
常見問題
為什麼卡片每次重新渲染都回到第一張?
元件在 cards 參照改變時會重置進度。如果在 JSX 裡直接寫陣列字面值,父層每次重新渲染都會產生新陣列;請把陣列放在元件外、存在 state,或用 useMemo 包起來。
可以在 Next.js App Router 使用嗎?
可以。原始碼開頭已有 "use client",在 Server Component 頁面直接 import 即可;資料可以在伺服器端取得後當成 props 傳給一個用戶端元件,再組成 cards。
onSwipe 什麼時候觸發?
卡片飛出的動畫結束後才觸發,帶入該卡的 id 與方向("left" 或 "right"),之後下一張補位。用 ref 呼叫 swipe("left") 或 swipe("right") 也會走同一個流程。
鍵盤與減少動態效果怎麼處理?
最上層卡片可以聚焦,右方向鍵表示喜歡、左方向鍵表示略過;後方卡片以 aria-hidden 隱藏。系統開啟「減少動態效果」時停用拖曳與旋轉,改用按鈕或鍵盤以淡出切換。
同時會渲染幾張卡片?
最多 3 張,後方卡片依深度縮小並下移。容器不會裁切內容,建議用 className 指定高度,卡片尺寸由 content 自己決定。