Drag Card Stack

It is a high-end, interactive UI component built with React and Framer Motion that mimics the tactile experience of a physical deck of cards. The core functionality relies on a physics-based drag engine: users can flick the top card off the stack, which triggers a state-reordering logic that moves the "dismissed" card to the bottom of the pile, enabling an infinite, cyclic loop of content.

Live Preview
FEATURED

Card 1

Swipe left or right to dismiss this card and reveal the next task in your stack.

PriorityHigh
FEATURED

Card 2

Swipe left or right to dismiss this card and reveal the next task in your stack.

PriorityHigh
FEATURED

Card 3

Swipe left or right to dismiss this card and reveal the next task in your stack.

PriorityHigh
FEATURED

Card 4

Swipe left or right to dismiss this card and reveal the next task in your stack.

PriorityHigh

Dependencies

npm installframer-motion

Code

"use client";
import React, { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";

const DragCardStack = () => {
// replace it by your data
  const [cards, setCards] = useState([
    {
      id: 1,
      color: "#FF0055",
      text: "Card 1",
      bgimg:
        "url('https://images.unsplash.com/vector-1759400775660-825488f4f480?q=80&w=2128&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D')",
    },
    {
      id: 2,
      color: "#0099FF",
      text: "Card 2",
      bgimg:
        "url('https://images.unsplash.com/vector-1738399643589-0d4500d75b38?q=80&w=2020&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D')",
    },
    {
      id: 3,
      color: "#22CC88",
      text: "Card 3",
      bgimg: "url('/myimg3-new.png')",
    },
    {
      id: 4,
      color: "#FF9900",
      text: "Card 4",
      bgimg: "url('/myimg2.jpg')",
    },
  ]);

  const moveToEnd = (fromIndex: any) => {
    setCards((prevCards) => {
      const newCards = [...prevCards];
      const item = newCards.splice(fromIndex, 1)[0];
      newCards.unshift(item);
      return newCards;
    });
  };

  return (
    <div className="relative h-120 flex justify-center items-center">
      <AnimatePresence>
        {cards.map((card, index) => {
          const isTop = index === cards.length - 1;

          return (
            <motion.div
              className="border-10 rounded-3xl overflow-hidden absolute flex h-[400px] w-[300px] cursor-grab items-center justify-center shadow-[0_10px_20px_rgba(0,0,0,0.2)]"
              key={card.id}
              style={{
                borderColor: card.color,
                zIndex: index,
              }}
              drag={isTop ? "x" : false}
              dragConstraints={{ left: 0, right: 0 }}
              onDragEnd={(event, info) => {
                if (Math.abs(info.offset.x) > 100) {
                  moveToEnd(index);
                }
              }}
              initial={{ scale: 0.9, opacity: 0 }}
              animate={{
                scale: 1 - (cards.length - 1 - index) * 0.05,
                y: (cards.length - 1 - index) * -15,
                opacity: 1,
              }}
              exit={{ scale: 0.8, opacity: 0 }}
              transition={{ type: "spring", stiffness: 300, damping: 30 }}
              whileDrag={{ scale: 1.05 }}
            >
              <motion.div
                style={{
                  backgroundImage: card.bgimg,
                }}
                className="relative h-full w-full flex flex-col justify-between overflow-hidden p-6 text-white box-border bg-cover bg-center bg-no-repeat relative"
              >
                <div className="absolute inset-0 h-full w-full bg-gradient-to-t from-black/80 via-black/60 to-transparent" />

                <div className="flex justify-between items-center z-10">
                  <span className="text-[10px] font-extrabold tracking-[1.2px] bg-white/20 px-2 py-1 rounded-[6px]">
                    FEATURED
                  </span>
                  <div
                    style={{ backgroundColor: card.color }}
                    className="h-2 w-2 rounded-full"
                  />
                </div>

                <div className="z-10 text-left">
                  <h2 className="mb-2 text-[28px] font-bold">{card.text}</h2>
                  <p className="m-0 text-sm leading-normal opacity-90">
                    Swipe left or right to dismiss this card and reveal the next
                    task in your stack.
                  </p>
                </div>

                <div className="flex items-center justify-between border-t border-white/20 pt-4 z-10">
                  <div className="flex flex-col">
                    <span style={{ fontSize: "12px", opacity: 0.8 }}>
                      Priority
                    </span>
                    <span style={{ fontWeight: "bold" }}>High</span>
                  </div>
                  <button
                    className="cursor-pointer rounded-[10px] bg-white px-4 py-2 text-[12px] font-semibold text-black border-none"
                    onClick={(e) => e.stopPropagation()}
                  >
                    View Details
                  </button>
                </div>
              </motion.div>
            </motion.div>
          );
        })}
      </AnimatePresence>
    </div>
  );
};

export default DragCardStack;

Props

PropTypeDefaultDescription
cardsCardItem[]initialCardsAn array of objects containing the id, color, text, and bgimg for each card in the stack.

Drag Card Stack Two

A sleek, interactive card stack component built with React, Tailwind CSS, and Framer Motion. It features a modern glassmorphic design and intuitive drag-and-drop mechanics that let users swipe through a layered stack of cards.

Live Preview

#Card 1

#Card 2

#Card 3

#Card 4

Dependencies

npm installframer-motion

Utils

import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

Code

"use client";
import React, { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { cn } from "@/lib/utils";

interface CardProps {
  card: {
    id: number;
    color: string;
    text: string;
    bgimg: string;
    className?: string;
  };
  index: number;
  totalCards: number;
  moveToEnd: (fromIndex: number) => void;
}

const Card = ({ card, index, totalCards, moveToEnd }: CardProps) => {
  const isTop = index === totalCards - 1;

  return (
    <motion.div
      className={cn(
        "overflow-hidden bg-white absolute flex cursor-grab items-center justify-center border-8 select-none",
        !card.className ? "h-[420px] w-[300px]" : card.className,
      )}
      style={{
        borderColor: card.color,
        zIndex: index,
        boxShadow: `0 15px 35px -10px rgba(0,0,0,0.15), inset 0 2px 14px ${card.color}25`,
      }}
      drag={isTop ? true : false}
      dragConstraints={{ left: 0, right: 0, top: 0, bottom: 0 }}
      onDragEnd={(event, info) => {
        if (Math.abs(info.offset.x) > 100 || Math.abs(info.offset.y) > 100) {
          moveToEnd(index);
        }
      }}
      initial={{ scale: 0.85, opacity: 0, y: 20 }}
      animate={{
        scale: 1 - (totalCards - 1 - index) * 0.05,
        y: (totalCards - 1 - index) * -16,
        opacity: 1,
      }}
      exit={{ scale: 0.8, opacity: 0 }}
      transition={{ type: "spring", stiffness: 300, damping: 25 }}
      whileHover={isTop ? { scale: 1.02, y: -10 } : {}}
      whileDrag={{
        scale: 1.04,
        boxShadow: "0 25px 50px -12px rgba(0,0,0,0.25)",
      }}
    >
      <div className="relative h-full w-full flex flex-col justify-between p-6 backdrop-blur-sm">
        <div
          style={{
            backgroundImage: card.bgimg,
          }}
          className="w-full shadow-[inset_0_4px_12px_rgba(0,0,0,0.3)] h-[400px] bg-cover bg-center bg-no-repeat z-10 border border-slate-200/50"
        />

        <h2 className="text-2xl font-black italic text-center text-slate-800 z-10 tracking-wide mt-4 uppercase">
          #{card.text}
        </h2>

        <div
          className="absolute inset-0 z-0 opacity-15 pointer-events-none rounded-3xl"
          style={{
            backgroundImage: `
              repeating-linear-gradient(
                45deg, 
                ${card.color}, 
                ${card.color} 1px, 
                transparent 1px, 
                transparent 5px
              ),
              repeating-linear-gradient(
                -45deg, 
                ${card.color}, 
                ${card.color} 1px, 
                transparent 1px, 
                transparent 5px
              )
            `,
          }}
        />
      </div>
    </motion.div>
  );
};

const DragCardStackTwo = () => {
  const [cards, setCards] = useState([
    {
      id: 1,
      className: "w-[400px] h-[400px] rotate-15",
      color: "#FF0055",
      text: "Card 1",
      bgimg:
        "url('https://images.unsplash.com/photo-1777131263706-d6c57f590a7c?q=80&w=1974&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D')",
    },
    {
      id: 2,
      className: "w-[400px] h-[400px] -rotate-15",
      color: "#0099FF",
      text: "Card 2",
      bgimg:
        "url('https://images.unsplash.com/photo-1777026050794-a5e4ef7cd254?q=80&w=2071&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D')",
    },
    {
      id: 3,
      className: "w-[400px] h-[400px] rotate-30",
      color: "#22CC88",
      text: "Card 3",
      bgimg:
        "url('https://images.unsplash.com/photo-1777067165450-60573b8afd54?q=80&w=1936&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D')",
    },
    {
      id: 4,
      className: "w-[400px] h-[400px] -rotate-30",
      color: "#FF9900",
      text: "Card 4",
      bgimg:
        "url('https://images.unsplash.com/photo-1776941654802-f29f34a675b9?q=80&w=1972&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D')",
    },
  ]);

  const moveToEnd = (fromIndex: number) => {
    setCards((prevCards) => {
      const newCards = [...prevCards];
      const item = newCards.splice(fromIndex, 1)[0];
      newCards.unshift(item);
      return newCards;
    });
  };

  return (
    <div className="relative h-[550px] flex justify-center items-center w-full rounded-3xl p-10">
      <AnimatePresence>
        {cards.map((card, index) => (
          <Card
            key={card.id}
            card={card}
            index={index}
            totalCards={cards.length}
            moveToEnd={moveToEnd}
          />
        ))}
      </AnimatePresence>
    </div>
  );
};

export default DragCardStackTwo;

Props

PropTypeDefaultDescription
cardsCardItem[]initialCardsAn array of objects containing the id, color, text, className and bgimg for each card in the stack.