Collaboration
Project Management
Finance
Deck
A Tinder-like swipeable card stack component with smooth animations.
Installation
npx love-ui@latest add deckUsage
import { Deck, DeckCards, DeckItem, DeckEmpty } from "@/components/deck"<Deck>
<DeckCards>
{items.map((item) => (
<DeckItem key={item.id}>
{item.content}
</DeckItem>
))}
</DeckCards>
<DeckEmpty>No more cards</DeckEmpty>
</Deck>Features
- Swipe cards left or right with touch or mouse drag
- Smooth, animated transitions powered by Motion
- Composable API: pass children as swipeable cards
- Customizable swipe threshold, stack size, perspective, and scaling
- Callbacks for swipe and swipe end events
- Visual stack effect with perspective and scaling
- Controlled and uncontrolled index support
- Automatic card progression after swipe
Examples
Controlled Deck
Current Index: 0 | Next Direction: left
Card 1
Swipe or use buttons
Card 2
Swipe or use buttons
Card 3
Swipe or use buttons
No more cards
"use client";
import { Deck, DeckCards, DeckEmpty, DeckItem } from "../../../../../packages/deck";
import { useState } from "react";
import { Button } from "../../../../../packages/ui/src/ui/button";
const cards = [
{ id: 1, title: "Card 1", color: "bg-red-500" },
{ id: 2, title: "Card 2", color: "bg-blue-500" },
{ id: 3, title: "Card 3", color: "bg-green-500" },
{ id: 4, title: "Card 4", color: "bg-yellow-500" },
{ id: 5, title: "Card 5", color: "bg-purple-500" },
];
const Example = () => {
const [currentIndex, setCurrentIndex] = useState(0);
const [animationDirection, setAnimationDirection] = useState<
"left" | "right"
>("left");
const nextCardLeft = () => {
if (currentIndex < cards.length) {
setAnimationDirection("left");
// Small delay to ensure direction is set before index changes
setTimeout(() => {
setCurrentIndex((prev) => prev + 1);
}, 0);
}
};
const nextCardRight = () => {
if (currentIndex < cards.length) {
setAnimationDirection("right");
// Small delay to ensure direction is set before index changes
setTimeout(() => {
setCurrentIndex((prev) => prev + 1);
}, 0);
}
};
return (
<div className="space-y-4">
<div className="text-center">
<p className="mb-2 text-muted-foreground text-sm">
Current Index: {currentIndex} | Next Direction: {animationDirection}
</p>
<div className="flex justify-center gap-2">
<Button
disabled={currentIndex >= cards.length}
onClick={nextCardLeft}
size="sm"
variant="outline"
>
Next (Left Animation)
</Button>
<Button
disabled={currentIndex >= cards.length}
onClick={nextCardRight}
size="sm"
variant="outline"
>
Next (Right Animation)
</Button>
</div>
</div>
<Deck className="mx-auto aspect-square w-40">
<DeckCards
animateOnIndexChange={true}
className="aspect-[2/3]"
currentIndex={currentIndex}
indexChangeDirection={animationDirection}
onCurrentIndexChange={setCurrentIndex}
onSwipe={(_index, _direction) => {
// Handle swipe action
}}
>
{cards.map((card) => (
<DeckItem
className={`${card.color} flex-col text-center text-white`}
key={card.id}
>
<h3 className="font-bold text-2xl">{card.title}</h3>
<p className="text-sm opacity-90">Swipe or use buttons</p>
</DeckItem>
))}
</DeckCards>
<DeckEmpty />
</Deck>
</div>
);
};
export default Example;