gidl
Components

Flip Cards

Flip cards of some of my favorite anime characters. Inspired by a Framer component

Card 1
Card 2
Card 3
Card 4
Card 5
Card 6
"use client";

import { cn } from "../../lib/utils";
import { motion, AnimatePresence, useMotionValue, PanInfo } from "motion/react";
import { RefObject, useRef, useState } from "react";
import { PlaceholderImage } from "next-image-placeholder/react";
import { getPlaceholderAction } from "@/actions/get-location-data";

type ImgItem = { id: number; url: string };

const CARD_IMAGES: ImgItem[] = [
{
id: 1,
url: "https://i.pinimg.com/736x/57/41/48/574148dab4a394849534eae05fe5628b.jpg",
},
{
id: 2,
url: "https://i.pinimg.com/736x/85/f3/61/85f361f859cd06b55e5d14fe09e4b9f8.jpg",
},
{
id: 3,
url: "https://i.pinimg.com/736x/4f/62/d2/4f62d2bc39cb6092fe4dedb978c285c5.jpg",
},
{
id: 4,
url: "https://i.pinimg.com/1200x/54/9e/0e/549e0e7ad7e492ba4766f0bbdfe5e0c8.jpg",
},
{
id: 5,
url: "https://i.pinimg.com/736x/5b/6d/ee/5b6dee0124a9ccb6e890357521648104.jpg",
},
{
id: 6,
url: "https://i.pinimg.com/736x/fd/ab/cb/fdabcb91d2df70207bb8740029cb8a18.jpg",
},
];

export const FlipCard = () => {
const constraintRef = useRef<HTMLDivElement | null>(null);
const [cards, setCards] = useState(CARD_IMAGES);

const handleCardDismiss = () => {
setCards((prev) => {
  const [first, ...rest] = prev;
  return [...rest, first];
});
};

return (
<div className="relative h-[400px] md:h-[500px] w-full overflow-hidden">
  <AnimatePresence initial={false}>
    {cards.map((card, index) => (
      <Card
        key={card.id}
        card={card}
        index={index}
        totalCards={cards.length}
        onDismiss={index === 0 ? handleCardDismiss : undefined}
      />
    ))}
  </AnimatePresence>
</div>
);
};

const Card = ({
card,
index,
totalCards,
onDismiss,
}: {
card: ImgItem;
index: number;
totalCards: number;
onDismiss?: () => void;
}) => {
const isTopCard = index === 0;
const rotation = -index * 7;

const x = useMotionValue(0);
const y = useMotionValue(0);

const handleDragEnd = (_: any, info: PanInfo) => {
if (onDismiss) {
  onDismiss();
  x.set(0);
  y.set(0);
}
};

return (
<motion.div
  className={cn(
    "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[150px] h-[250px] md:w-[220px] md:h-[320px]",
    isTopCard ? "cursor-grab active:cursor-grabbing" : "cursor-default"
  )}
  initial={false}
  style={{
    x,
    y,
    zIndex: totalCards - index,
    touchAction: isTopCard ? "none" : "auto",
  }}
  animate={{
    rotate: rotation,
  }}
  transition={{
    type: "spring",
    stiffness: 400,
    damping: 30,
  }}
  drag={isTopCard}
  dragConstraints={{ top: 0, right: 0, bottom: 0, left: 0 }}
  dragElastic={0.5}
  dragTransition={{
    bounceStiffness: 700,
    bounceDamping: 40,
  }}
  whileHover={isTopCard ? { scale: 1.03 } : undefined}
  onDragEnd={isTopCard ? handleDragEnd : undefined}
>
  <PlaceholderImage
    src={card.url}
    action={getPlaceholderAction}
    alt={`Card ${card.id}`}
    fill
    className="pointer-events-none rounded-xl object-cover"
    style={{
      userSelect: "none",
      // @ts-ignore
      WebkitUserDrag: "none",
    }}
  />
</motion.div>
);
};

About

Built to emulate dynamic fluid motion natively using Framer Motion and Radix UI primitives. Ensures seamless integration with existing React architectures.

Installation

npx gidl add flip-cards

Install the following dependencies:

npm install motion/react

Copy and paste the code into your project.

Update the import paths to match your project setup.

Usage

import { FlipCard } from "@/components/ui/flip-cards";

export default function App() {
  return <FlipCard />;
}

On this page