bundui svg logo
Bundui

Flip Card

Experience smooth, fluid card navigation with natural drag interactions and beautiful animations

Card 1
Card 2
Card 3
Card 4
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

PropTypeDefault
shadowIntensity?
number
0.2
borderRadius?
number
10
dragThreshold?
number
120
stackRotation?
number
8
stackOffset?
number
20
children?
ReactNode[]
-