Components
Draggable card stack with spring physics — swipe cards left or right to dismiss them. Cards fan out in a stacked formation with depth scaling. Drag triggers rotation proportional to horizontal offset. Flick detection uses velocity threshold for snappy gestures. Swipe indicators fade in as you drag. Callbacks fire on each swipe with direction and index. Configurable stack depth, scale step, rotation factor, swipe threshold, and custom indicators. Fully responsive DOM-based rendering. Built on Framer Motion springs.
Loading preview...
import { Component as SwipeStack } from "@/components/ui/swipe-stack";
import { useState } from "react";
import { Heart, X, Star, Quote, RotateCcw } from "lucide-react";
const TESTIMONIALS = [
{
quote: "This completely transformed our workflow. We shipped 3x faster within the first month.",
name: "Sarah Chen",
role: "CTO at Vercel",
avatar: "SC",
color: "from-violet-500 to-indigo-500",
stars: 5,
},
{
quote: "The best developer tool I have used in years. Clean API, incredible performance.",
name: "Marcus Rivera",
role: "Lead Engineer at Stripe",
avatar: "MR",
color: "from-cyan-500 to-blue-500",
stars: 5,
},
{
quote: "Our team adopted it overnight. Zero learning curve, massive productivity gains.",
name: "Aiko Tanaka",
role: "VP of Engineering at Linear",
avatar: "AT",
color: "from-emerald-500 to-teal-500",
stars: 5,
},
{
quote: "I was skeptical at first but the results speak for themselves. 10x improvement.",
name: "James O'Brien",
role: "Founder at Railway",
avatar: "JO",
color: "from-orange-500 to-amber-500",
stars: 4,
},
{
quote: "Finally, a tool that just works. No config, no setup, just results.",
name: "Priya Sharma",
role: "Staff Engineer at Figma",
avatar: "PS",
color: "from-pink-500 to-rose-500",
stars: 5,
},
];
function TestimonialCard({
quote,
name,
role,
avatar,
color,
stars,
}: (typeof TESTIMONIALS)[number]) {
return (
<div className="w-full h-full rounded-2xl border border-white/10 bg-neutral-900 p-8 flex flex-col justify-between overflow-hidden relative">
{/* Gradient accent */}
<div
className={`absolute top-0 left-0 right-0 h-1 bg-gradient-to-r ${color}`}
/>
<div>
<Quote className="size-8 text-white/10 mb-4" />
<p className="text-white text-lg leading-relaxed font-medium">
{quote}
</p>
<div className="flex gap-0.5 mt-4">
{Array.from({ length: 5 }).map((_, i) => (
<Star
key={i}
className={`size-4 ${i < stars ? "text-amber-400 fill-amber-400" : "text-neutral-700"}`}
/>
))}
</div>
</div>
<div className="flex items-center gap-3 mt-6">
<div
className={`size-10 rounded-full bg-gradient-to-br ${color} flex items-center justify-center text-white text-xs font-bold`}
>
{avatar}
</div>
<div>
<p className="text-sm font-semibold text-white">{name}</p>
<p className="text-xs text-neutral-400">{role}</p>
</div>
</div>
</div>
);
}
export default function Demo() {
const [swiped, setSwiped] = useState<{ dir: string; idx: number }[]>([]);
const [key, setKey] = useState(0);
const handleSwipe = (dir: "left" | "right", idx: number) => {
setSwiped((prev) => [...prev, { dir, idx }]);
};
const handleReset = () => {
setSwiped([]);
setKey((k) => k + 1);
};
const likes = swiped.filter((s) => s.dir === "right").length;
const nopes = swiped.filter((s) => s.dir === "left").length;
return (
<div className="flex min-h-screen flex-col items-center justify-center bg-[#030712] p-8">
<div className="w-full max-w-md mx-auto">
<div className="text-center mb-8">
<h2 className="text-2xl font-bold tracking-tight text-white mb-2">
Swipe Stack
</h2>
<p className="text-sm text-neutral-400">
Drag cards left or right. Flick to dismiss.
</p>
</div>
{/* Stats */}
<div className="flex justify-center gap-6 mb-6">
<div className="flex items-center gap-2">
<div className="size-8 rounded-full bg-red-500/10 border border-red-500/20 flex items-center justify-center">
<X className="size-4 text-red-400" />
</div>
<span className="text-sm font-mono text-neutral-400">{nopes}</span>
</div>
<div className="flex items-center gap-2">
<div className="size-8 rounded-full bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center">
<Heart className="size-4 text-emerald-400" />
</div>
<span className="text-sm font-mono text-neutral-400">{likes}</span>
</div>
</div>
{/* Stack */}
<SwipeStack
key={key}
className="w-full h-[360px]"
onSwipe={handleSwipe}
swipeThreshold={80}
visibleCount={3}
scaleStep={0.04}
offsetStep={10}
rotationFactor={12}
>
{TESTIMONIALS.map((t, i) => (
<TestimonialCard key={i} {...t} />
))}
</SwipeStack>
{/* Reset button */}
{swiped.length >= TESTIMONIALS.length && (
<div className="flex justify-center mt-8">
<button
onClick={handleReset}
className="flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-5 py-2.5 text-sm font-medium text-white hover:bg-white/10 transition-colors cursor-pointer"
>
<RotateCcw className="size-4" />
Start over
</button>
</div>
)}
</div>
</div>
);
}