Swipe Card Stack
Chồng thẻ quẹt kiểu Tinder — kéo, búng, ném đi, có dấu đóng, hoàn tác và vật lý thật
.zip · HTML / CSS / JS · v1.0.0
Miễn phí — không cần tài khoản hay email. Luôn là bản mới nhất.
Đang chạy trực tiếp
Giới thiệu
Kiểu quẹt ai cũng đã quen tay, giờ có cảm giác như cầm thẻ thật. Nắm thẻ trên cùng là nó bám theo ngón tay, nghiêng theo góc bạn cầm; một con dấu hiện dần cho biết thẻ đang về phía nào. Thả tay khi đã qua vạch — hoặc búng một cái — là thẻ bay khỏi màn hình đúng với lực bạn ném. Thả sớm quá thì thẻ bật về, rung nhẹ một chút. Các thẻ phía sau xếp xoè chờ sẵn và nhô lên khi thẻ trên cùng rời đi.
- Vật lý chỉnh được: kéo bao xa, búng bao nhanh, nghiêng bao nhiêu, bay trong bao lâu
- Sinh ra để chọn: điểm đến, sản phẩm, ứng viên, flashcard, mẹo hướng dẫn
- Nút bấm, phím mũi tên và code của bạn đều quẹt với cùng một chuyển động; hoàn tác đưa thẻ quay lại
- Chế độ lặp cho chồng thẻ không bao giờ hết, và ô hiển thị riêng cho lúc đã hết thẻ
- Mọi màu sắc, kích thước, thời gian đều là CSS custom property; sáng tối tự theo trang của bạn
- Không cần cài gì: một file CSS và một script nhỏ, hoặc một component React
Tính năng · 9
- Kéo bằng chuột, ngón tay hay bút: thẻ bám theo, nghiêng theo và bật về nếu thả tay quá sớm
- Kéo qua vạch hoặc búng đủ nhanh là thẻ bay khỏi màn hình với đúng tốc độ bạn ném
- Dấu NOPE / LIKE / SUPER hiện dần khi kéo — chữ trên dấu đổi tuỳ ý
- Các thẻ phía sau xếp có chiều sâu và nhô lên khi thẻ trên cùng rời đi
- Nút bấm, phím mũi tên và code (swipe, undo, reset) dùng chung một chuyển động
- Hoàn tác đưa thẻ vừa quẹt quay lại, kể cả khi nó đang bay; có chế độ lặp và ô hiển thị khi hết thẻ
- Sự kiện swipe, undo và empty kèm hướng, thẻ và vị trí
- Bản HTML/CSS/JS thuần và bản React (dùng được với Next.js, nhận children hoặc items + render prop, có ref API)
- Không phụ thuộc thư viện, khoảng 8 KB sau gzip (cả script lẫn style)
Cách dùng
Gắn stylesheet và script một lần, rồi đánh dấu chồng thẻ bằng data-swipe-stack. Mỗi thẻ là một
.swipe-stack__card nằm trong .swipe-stack__deck; nút là <button> bất kỳ có
data-swipe-stack-action:
<link rel="stylesheet" href="swipe-stack.css" />
<script src="swipe-stack.js" defer></script>
<section class="swipe-stack" data-swipe-stack data-swipe-stack-directions="left right up" aria-label="Điểm đến">
<div class="swipe-stack__deck">
<article class="swipe-stack__card" data-swipe-title="Hội An">…</article>
<article class="swipe-stack__card" data-swipe-title="Đà Lạt">…</article>
<div class="swipe-stack__empty">Hết rồi!</div>
</div>
<div class="swipe-stack__actions">
<button class="swipe-stack__action" type="button" data-swipe-stack-action="undo" aria-label="Hoàn tác">…</button>
<button class="swipe-stack__action" type="button" data-swipe-stack-action="left" aria-label="Bỏ qua">…</button>
<button class="swipe-stack__action" type="button" data-swipe-stack-action="up" aria-label="Siêu thích">…</button>
<button class="swipe-stack__action" type="button" data-swipe-stack-action="right" aria-label="Thích">…</button>
</div>
</section>
<script>
document.querySelector(".swipe-stack").addEventListener("swipe", (event) => {
const { direction, index, card } = event.detail; // "left" | "right" | "up"
});
</script>Tuỳ chọn qua data attribute:
| Attribute | Mặc định | Tác dụng |
|---|---|---|
data-swipe-stack-directions |
"left right" |
Thẻ được quẹt về những hướng nào; thêm up để có siêu thích |
data-swipe-stack-loop |
tắt | Hết thẻ thì quay lại thẻ đầu thay vì hiện ô hết thẻ |
data-swipe-stack-visible |
3 |
Hiện bao nhiêu thẻ cùng lúc, tính cả thẻ trên cùng |
data-swipe-stack-threshold |
0.35 |
Phải kéo bao xa (tính theo bề ngang thẻ; theo chiều cao nếu kéo lên) |
data-swipe-stack-velocity |
0.45 |
Búng nhanh cỡ nào thì thẻ đi luôn dù kéo ngắn (px mỗi ms) |
data-swipe-stack-rotation |
14 |
Thẻ nghiêng bao nhiêu độ khi kéo được một bề ngang thẻ |
data-swipe-stack-duration |
420 |
Thời gian lâu nhất để thẻ bay đi hoặc quay lại (ms) |
data-swipe-stack-stamp-left, -right, -up |
NOPE, LIKE, SUPER |
Chữ trên các con dấu |
Chồng thẻ phát các sự kiện swipe, undo (cả hai kèm { direction, index, card }) và empty
ngay trên phần tử của nó. Tên được đọc lên của một thẻ là data-swipe-title, nếu không có thì
aria-label, rồi đến tiêu đề đầu tiên trong thẻ. Phần tử nào trong thẻ không được phép bắt đầu thao
tác kéo (thanh trượt, bản đồ…) thì thêm data-swipe-stack-no-drag.
SwipeStack.get(element) trả về handle của chồng thẻ đã được gắn —
{ swipe(direction), undo(), reset(), refresh(), canUndo(), current(), destroy() }; nhớ gọi
refresh() sau khi thêm hoặc bớt thẻ. Chồng thẻ thêm vào sau khi trang đã tải:
SwipeStack.init(container) — hàm này trả về một hàm để gỡ ra. Muốn truyền callback hay câu chữ đã
dịch thì tự dựng: SwipeStack.create(element, { directions, loop, onSwipe, onUndo, onEmpty, texts })
(mọi câu chữ nó nói nằm trong core/texts.ts).
Thẻ bay sang ngang sẽ vượt qua mép màn hình: thêm overflow-x: clip cho trang
(html, body { overflow-x: clip; }) để trang không bị nới rộng ra trong chốc lát.
Chép SwipeStack.tsx, swipe-stack.css và thư mục core vào dự án:
import { useRef } from "react";
import { SwipeStack } from "./SwipeStack";
export function Picker({ places }) {
const stack = useRef(null);
return (
<>
<SwipeStack
ref={stack}
aria-label="Điểm đến"
items={places}
itemKey={(place) => place.id}
itemTitle={(place) => place.name}
renderItem={(place) => <PlaceCard place={place} />}
directions={["left", "right", "up"]}
onSwipe={({ direction, item }) => save(item, direction)}
empty={<p>Hết rồi!</p>}
/>
<button type="button" onClick={() => stack.current?.undo()}>Hoàn tác</button>
</>
);
}Hoặc truyền thẻ dưới dạng children: <SwipeStack>{cards}</SwipeStack>, mỗi child là một thẻ. Ref
cho bạn { swipe(direction), undo(), reset() }. Mọi tuỳ chọn ở trên đều là prop (directions,
loop, visible, threshold, velocity, rotation, duration, texts), thêm onSwipe, onUndo
và onEmpty (onSwipe nhận cả item), actions={false} để bỏ các nút có sẵn, và mọi prop của
<section> (className, aria-*…). React 18 hoặc 19.
Component bắt đầu bằng "use client", nên dùng được trong mọi page hay server component. Với App
Router không cần làm gì thêm. Với Pages Router, chuyển dòng import CSS vào pages/_app.
Đặt các biến sau trên .swipe-stack (hoặc class của bạn):
| Biến | Đổi gì |
|---|---|
--swipe-stack-width, --swipe-stack-ratio, --swipe-stack-radius |
Kích thước, tỉ lệ và bo góc của thẻ |
--swipe-stack-surface, --swipe-stack-border, --swipe-stack-shadow, --swipe-stack-text |
Bản thân tấm thẻ |
--swipe-stack-left, --swipe-stack-right, --swipe-stack-up, --swipe-stack-undo |
Màu dấu và nút, theo từng hướng |
--swipe-stack-stamp-surface |
Nền phía sau chữ trên dấu |
--swipe-stack-offset, --swipe-stack-scale, --swipe-stack-fan |
Các thẻ phía sau lùi xuống, thu nhỏ và xoè ra bao nhiêu |
--swipe-stack-settle, --swipe-stack-ease |
Các thẻ phía sau nhô lên nhanh chậm ra sao |
--swipe-stack-button-size, --swipe-stack-button-small, --swipe-stack-button-surface |
Các nút bấm |
--swipe-stack-ring, --swipe-stack-gap |
Viền focus, và khoảng cách giữa chồng thẻ với hàng nút |
Màu dùng light-dark(): tự theo color-scheme của trang. Khi bật prefers-reduced-motion, thẻ
được quẹt mờ dần tại chỗ thay vì bay đi, thẻ đang kéo không nghiêng, và các thẻ phía sau nhô lên mà
không trượt.
Hỏi về sản phẩm này
Chưa chắc có hợp dự án của bạn? Cứ hỏi — câu trả lời lấy từ chính trang này.
Lịch sử phiên bản
1.0.0First releaseMới nhất
Plain HTML/CSS/JS and React builds. Drag or flick cards with mouse, touch or pen, stamps, buttons, keyboard, undo, loop mode, announcements and reduced-motion support.
- Bản sửa lỗi và tính năng mới đều có ngay ở nút tải này — file tải về luôn là bản mới nhất.
Có thể bạn sẽ thích
- ComponentThẻ
Spotlight Card
Thẻ kính mờ với vệt sáng dịu bám theo con trỏ — và lan sang cả lưới thẻ
Miễn phí


