Sarah
28New York City
Adventure seeker and coffee enthusiast. Love exploring new places and trying different cuisines.
Alex
31San Francisco
Tech entrepreneur by day, musician by night. Looking for someone to share adventures with.
Emily
26London
Art curator with a passion for contemporary design. Always up for gallery visits and creative discussions.
Michael
29Los Angeles
Film director seeking inspiration. Let's create something beautiful together.
Sophie
27Paris
Pastry chef with a sweet tooth. Looking for someone to share my culinary experiments with.
Julia
25Berlin
Graphic designer with a passion for typography. Always looking for new design challenges.
Chris
30Chicago
Entrepreneur with a passion for innovation. Looking for someone to help me change the world.
Laura
29Sydney
Yoga instructor with a love for nature. Always looking for new outdoor adventures.
Jenny
26Tokyo
Fashion designer with a passion for style. Let's explore the city together.
Matthew
32Melbourne
Software engineer with a love for coffee. Looking for someone to share a cuppa with.
Kate
27Rome
Art historian with a passion for museums. Always looking for new art to admire.
Adam
31Toronto
Entrepreneur with a passion for innovation. Looking for someone to help me change the world.
Lily
28Beijing
Photographer with a love for nature. Always looking for new landscapes to capture.
Lucas
29Amsterdam
Graphic designer with a passion for illustration. Looking for someone to share my art with.
Hannah
25Copenhagen
Fashion designer with a love for style. Let's explore the city together.
Installation
Copy and paste the following code into your project.
components/ui/swipe-cards.tsx
"use client";
import * as React from "react";
import { motion, useMotionValue, useTransform } from "framer-motion";
import { cn } from "@/lib/utils";
import Image from "next/image";
export interface CardData {
id: number;
url: string;
name?: string;
age?: number;
location?: string;
bio?: string;
interests?: string[];
}
export interface SwipeCardsProps extends React.HTMLAttributes<HTMLDivElement> {
data: CardData[];
onSwipe?: (id: number, direction: "left" | "right") => void;
className?: string;
}
export interface CardProps extends CardData {
cards: CardData[];
setCards: React.Dispatch<React.SetStateAction<CardData[]>>;
onSwipe?: (id: number, direction: "left" | "right") => void;
className?: string;
}
export function SwipeCards({
data,
onSwipe,
className,
...props
}: SwipeCardsProps) {
const [cards, setCards] = React.useState<CardData[]>(data);
return (
<div
className={cn(
"relative grid h-full w-full place-items-center",
className,
)}
{...props}
>
{cards.map((card) => (
<Card
key={card.id}
cards={cards}
setCards={setCards}
onSwipe={onSwipe}
{...card}
/>
))}
{cards.length === 0 && (
<div className="text-muted-foreground">No more profiles</div>
)}
</div>
);
}
function Card({
id,
url,
name,
age,
location,
bio,
interests,
setCards,
cards,
onSwipe,
className,
}: CardProps) {
const x = useMotionValue(0);
const rotateRaw = useTransform(x, [-150, 150], [-18, 18]);
const opacity = useTransform(x, [-150, 0, 150], [0, 1, 0]);
const isFront = id === cards[cards.length - 1].id;
const rotate = useTransform(() => {
const offset = isFront ? 0 : id % 2 ? 6 : -6;
return `${rotateRaw.get() + offset}deg`;
});
const handleDragEnd = () => {
const xVal = x.get();
if (Math.abs(xVal) > 100) {
setCards((pv) => pv.filter((v) => v.id !== id));
onSwipe?.(id, xVal > 0 ? "right" : "left");
}
};
const [imageError, setImageError] = React.useState(false);
return (
<motion.div
className={cn(
"relative h-96 w-72 origin-bottom select-none rounded-lg shadow-lg",
isFront && "cursor-grab active:cursor-grabbing",
className,
)}
style={{
gridRow: 1,
gridColumn: 1,
x,
opacity,
rotate,
transition: "0.125s transform",
touchAction: "none",
boxShadow: isFront
? "0 20px 25px -5px rgb(0 0 0 / 0.5), 0 8px 10px -6px rgb(0 0 0 / 0.5)"
: undefined,
}}
animate={{
scale: isFront ? 1 : 0.98,
}}
drag={isFront ? "x" : false}
dragConstraints={{ left: -1000, right: 1000 }}
dragElastic={0.15}
dragSnapToOrigin
onDragEnd={handleDragEnd}
whileTap={{ scale: 0.98 }}
whileDrag={{ scale: 1.02 }}
>
{imageError ? (
<div className="flex h-full w-full items-center justify-center rounded-lg bg-muted text-muted-foreground">
Failed to load image
</div>
) : (
<div className="relative h-full w-full overflow-hidden rounded-lg">
<Image
src={url}
alt={`${name}'s profile`}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
className="pointer-events-none object-cover"
onError={() => setImageError(true)}
priority={isFront}
/>
<div className="absolute inset-x-0 bottom-0 bg-linear-to-t from-black/80 to-transparent p-4 text-white">
<div className="flex items-center gap-2">
{name && <h3 className="text-xl font-semibold">{name}</h3>}
{age && <span className="text-lg">{age}</span>}
</div>
{location && <p className="text-sm text-gray-200">{location}</p>}
{bio && <p className="mt-2 line-clamp-2 text-sm">{bio}</p>}
{interests && interests.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{interests.map((interest) => (
<span
key={interest}
className="rounded-full bg-white/20 px-2 py-0.5 text-xs"
>
{interest}
</span>
))}
</div>
)}
</div>
</div>
)}
</motion.div>
);
}
Update the import paths to match your project setup.
import { SwipeCards } from "@/components/ui/swipe-cards";
Usage
import { SwipeCards } from "@/components/ui/swipe-cards";
const cardData = [
{
id: 1,
name: "Sarah",
age: 28,
location: "New York City",
bio: "Adventure seeker and coffee enthusiast.",
interests: ["Travel", "Photography", "Cooking"],
url: "path/to/image.jpg",
},
// ... more cards
];
export default function Demo() {
return (
<div className="h-[500px] w-full">
<SwipeCards
data={cardData}
onSwipe={(id, direction) => {
console.log(`Card ${id} swiped ${direction}`);
}}
/>
</div>
);
}
Props
SwipeCards
Prop | Type | Description |
---|---|---|
data | CardData[] | Array of card data to display |
onSwipe | (id: number, direction: "left" | "right") => void | Callback when a card is swiped |
className | string | Optional CSS class to style the container |
CardData
Property | Type | Description |
---|---|---|
id | number | Unique identifier for the card |
url | string | Image URL for the card |
name | string? | Optional name to display |
age | number? | Optional age to display |
location | string? | Optional location to display |
bio | string? | Optional biography text |
interests | string[]? | Optional array of interests |
Examples
Basic Cards
<SwipeCards
data={[
{
id: 1,
url: "path/to/image.jpg",
},
// ... more cards
]}
/>
Profile Cards
<SwipeCards
data={[
{
id: 1,
name: "Alex",
age: 28,
location: "San Francisco",
bio: "Tech enthusiast and coffee lover",
interests: ["Technology", "Coffee", "Travel"],
url: "path/to/profile.jpg",
},
// ... more profiles
]}
onSwipe={(id, direction) => {
// Handle swipe action
if (direction === "right") {
// Handle right swipe (e.g., like)
} else {
// Handle left swipe (e.g., pass)
}
}}
/>