Flip Card
Experience smooth, fluid card navigation with natural drag interactions and beautiful animations
import FlipCard from "@/components/core/flip-card";
const images = [
"https://images.unsplash.com/photo-1629394661462-13ea8fe156ef?q=80&w=1287&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"https://images.unsplash.com/photo-1535083783855-76ae62b2914e?q=80&w=1470&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"https://images.unsplash.com/photo-1497206365907-f5e630693df0?q=80&w=2080&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"https://images.unsplash.com/photo-1500479694472-551d1fb6258d?q=80&w=2940&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
];
export default function FlipCardExample() {
return (
<div className="h-72 w-60 pt-10">
<FlipCard>
{images.map((src, index) => (
<div key={index} className="h-72 w-60">
<img
className="pointer-events-none h-full w-full object-cover"
src={src}
alt={`Card ${index + 1}`}
/>
</div>
))}
</FlipCard>
</div>
);
}
Installation
Install the following dependencies:
npm i motion
Copy and paste the following code into your project:
"use client";
import React, { useState, useCallback, useMemo, ReactNode } from "react";
import { motion, AnimatePresence, PanInfo } from "motion/react";
type FlipCardProps = {
children: ReactNode[];
stackOffset?: number;
stackRotation?: number;
dragThreshold?: number;
borderRadius?: number;
shadowIntensity?: number;
};
export default function FlipCard({
children,
stackOffset = 20,
stackRotation = 8,
dragThreshold = 120,
borderRadius = 10,
shadowIntensity = 0.2
}: FlipCardProps) {
const childCount = React.Children.count(children);
const [cardOrder, setCardOrder] = useState(() => Array.from({ length: childCount }, (_, i) => i));
const getCardTransform = useCallback(
(index: number, childIndex: number) => {
const stackPosition = cardOrder.indexOf(childIndex);
const positionFromBottom = cardOrder.length - 1 - stackPosition;
return {
zIndex: stackPosition,
y: -positionFromBottom * stackOffset,
rotate: positionFromBottom * stackRotation,
scale: 1 - positionFromBottom * 0.02,
opacity: 1
};
},
[cardOrder, stackOffset, stackRotation]
);
const handleDragEnd = useCallback(
(event: any, info: PanInfo, cardIndex: number) => {
const dragDistance = Math.abs(info.offset.x) + Math.abs(info.offset.y);
const velocity = Math.abs(info.velocity.x) + Math.abs(info.velocity.y);
if (dragDistance > dragThreshold || velocity > 800) {
setCardOrder((prevOrder) => {
const newOrder = [...prevOrder];
const draggedCardPosition = newOrder.indexOf(cardIndex);
const draggedCard = newOrder.splice(draggedCardPosition, 1)[0];
newOrder.unshift(draggedCard);
return newOrder;
});
}
},
[dragThreshold]
);
const cardVariants = {
initial: (custom: any) => ({ ...custom, x: 0, y: custom.y }),
animate: (custom: any) => ({
...custom,
x: 0,
y: custom.y,
transition: {
type: "spring",
damping: 30,
stiffness: 500,
mass: 0.5,
restDelta: 0.01,
restSpeed: 0.01
}
}),
drag: {
scale: 1.05,
rotate: 0,
transition: { duration: 0.05 }
}
};
const renderedCards = useMemo(() => {
return React.Children.map(children, (child, index) => {
const transform = getCardTransform(index, index);
const isTopCard = cardOrder.indexOf(index) === cardOrder.length - 1;
return (
<motion.div
key={`card-${index}`}
custom={transform}
variants={cardVariants}
initial="initial"
animate="animate"
exit="exit"
whileDrag="drag"
drag={isTopCard}
dragConstraints={{
top: -150,
bottom: 150,
left: -150,
right: 150
}}
dragElastic={0.2}
dragSnapToOrigin={true}
dragTransition={{
power: 0.3,
timeConstant: 125,
bounceStiffness: 500,
bounceDamping: 30
}}
onDragEnd={(event, info) => handleDragEnd(event, info, index)}
className={`absolute overflow-hidden ${isTopCard ? "cursor-grab" : "cursor-default"}`}
style={{
borderRadius,
zIndex: transform.zIndex,
boxShadow: `0px ${4 + transform.zIndex * 2}px ${
8 + transform.zIndex * 4
}px rgba(0,0,0,${shadowIntensity})`
}}>
{child}
</motion.div>
);
});
}, [children, cardOrder, borderRadius, shadowIntensity, getCardTransform]);
return <AnimatePresence>{renderedCards}</AnimatePresence>;
}
Update the import paths to match your project setup.
Usage
import FlipCard from "@/components/core/flip-card";
const images = [
"https://images.unsplash.com/photo-1629394661462-13ea8fe156ef?q=80&w=1287&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"https://images.unsplash.com/photo-1535083783855-76ae62b2914e?q=80&w=1470&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"https://images.unsplash.com/photo-1497206365907-f5e630693df0?q=80&w=2080&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"https://images.unsplash.com/photo-1500479694472-551d1fb6258d?q=80&w=2940&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
];
export default function FlipCardExample() {
return (
<div className="h-72 w-60 pt-10">
<FlipCard>
{images.map((src, index) => (
<div key={index} className="h-72 w-60">
<img
className="pointer-events-none h-full w-full object-cover"
src={src}
alt={`Card ${index + 1}`}
/>
</div>
))}
</FlipCard>
</div>
);
}
Props
Prop | Type | Default |
---|---|---|
shadowIntensity? | number | 0.2 |
borderRadius? | number | 10 |
dragThreshold? | number | 120 |
stackRotation? | number | 8 |
stackOffset? | number | 20 |
children? | ReactNode[] | - |
Floating Button
A floating button is a common UI component used in user interfaces. It typically appears as a button "floating" over other content, often positioned in a corner of the screen. This button allows users to quickly perform a key action.
Image Comparison
Interactively compare two images with a draggable slider to reveal differences.