Flip Card

Base component for the FlipCard displaying front and back images.

Example

Click card to flip it
Front of the Card
Back of the Card

Installation

Copy and paste the source code into your project.

"use client";
 
import React, {
  useState,
  useRef,
  useEffect,
  ComponentType,
  HTMLAttributes,
} from "react";
import { motion, useSpring } from "framer-motion";
import Image from "next/image";
 
const spring = {
  type: "spring",
  stiffness: 300,
  damping: 40,
};
 
interface WithClickProps extends HTMLAttributes<HTMLDivElement> {
  variant?: "front" | "back";
  width: string;
  height: string;
  frontImage: string;
  backImage: string;
}
 
/**
 * Higher-Order Component to add click and flip functionality to a card component.
 *
 * @param {ComponentType<WithClickProps>} Component - The component to wrap.
 * @returns {ComponentType<WithClickProps>} The wrapped component with flip functionality.
 */
export function withClick(
  Component: ComponentType<WithClickProps>
): ComponentType<WithClickProps> {
  const WrappedComponent: React.FC<WithClickProps> = (props) => {
    const [isFlipped, setIsFlipped] = useState(false);
 
    const handleClick = () => {
      setIsFlipped((prevState) => !prevState);
    };
 
    const [rotateXaxis, setRotateXaxis] = useState(0);
    const [rotateYaxis, setRotateYaxis] = useState(0);
    const ref = useRef<HTMLDivElement>(null);
 
    const handleMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {
      const element = ref.current;
      if (!element) return;
 
      const elementRect = element.getBoundingClientRect();
      const elementWidth = elementRect.width;
      const elementHeight = elementRect.height;
      const elementCenterX = elementWidth / 2;
      const elementCenterY = elementHeight / 2;
      const mouseX = event.clientY - elementRect.y - elementCenterY;
      const mouseY = event.clientX - elementRect.x - elementCenterX;
      const degreeX = (mouseX / elementWidth) * 20;
      const degreeY = (mouseY / elementHeight) * 20;
      setRotateXaxis(degreeX);
      setRotateYaxis(degreeY);
    };
 
    const handleMouseEnd = () => {
      setRotateXaxis(0);
      setRotateYaxis(0);
    };
 
    const dx = useSpring(0, spring);
    const dy = useSpring(0, spring);
 
    useEffect(() => {
      dx.set(-rotateXaxis);
      dy.set(rotateYaxis);
    }, [dx, dy, rotateXaxis, rotateYaxis]);
 
    return (
      <motion.div
        className="cursor-pointer"
        onClick={handleClick}
        transition={spring}
        style={{
          perspective: "1500px",
          width: props.width,
          height: props.height,
        }}
      >
        <motion.div
          ref={ref}
          whileHover={{ scale: 1.1 }}
          onMouseMove={handleMouseMove}
          onMouseLeave={handleMouseEnd}
          transition={spring}
          style={{
            width: "100%",
            height: "100%",
            rotateX: dx,
            rotateY: dy,
            transformStyle: "preserve-3d",
            transformOrigin: "center center",
            position: "relative",
          }}
        >
          <motion.div
            animate={{ rotateY: isFlipped ? 180 : 0 }}
            transition={{ duration: 0.5, ease: "easeInOut" }}
            style={{
              width: "100%",
              height: "100%",
              backfaceVisibility: "hidden",
              position: "absolute",
              top: 0,
              left: 0,
              transformStyle: "preserve-3d",
            }}
          >
            <Component
              {...props}
              variant="front"
              style={{
                width: "100%",
                height: "100%",
              }}
            />
          </motion.div>
          <motion.div
            initial={{ rotateY: -180 }}
            animate={{ rotateY: isFlipped ? 0 : -180 }}
            transition={{ duration: 0.5, ease: "easeInOut" }}
            style={{
              width: "100%",
              height: "100%",
              backfaceVisibility: "hidden",
              position: "absolute",
              top: 0,
              left: 0,
              transformStyle: "preserve-3d",
            }}
          >
            <Component
              {...props}
              variant="back"
              style={{
                width: "100%",
                height: "100%",
              }}
            />
          </motion.div>
        </motion.div>
      </motion.div>
    );
  };
 
  WrappedComponent.displayName = `withClick(${Component.displayName || Component.name || "Component"})`;
 
  return WrappedComponent;
}
 
const FlipCardBase: React.FC<WithClickProps> = ({
  variant = "front",
  frontImage,
  backImage,
}) => {
  return variant === "front" ? (
    <div className="relative h-full w-full rounded-lg border border-gray-400 bg-white">
      <Image
        src={frontImage}
        width={500}
        height={1000}
        alt="Front of the Card"
        className="absolute inset-0 h-full w-full object-cover rounded-lg"
      />
    </div>
  ) : (
    <div className="relative h-full w-full rounded-lg flex items-center justify-center">
      <Image
        src={backImage}
        width={500}
        height={1000}
        alt="Back of the Card"
        className="absolute inset-0 h-full w-full object-cover rounded-lg"
      />
    </div>
  );
};
 
FlipCardBase.displayName = "FlipCardBase";
 
/**
 * Component FlipCard displaying front and back images.
 *
 * @param {"front" | "back"} props.variant - Indicates whether it's the front or back of the card.
 * @param {string} props.width - Width of the card.
 * @param {string} props.height - Height of the card.
 * @param {string} props.frontImage - URL of the front image of the card.
 * @param {string} props.backImage - URL of the back image of the card.
 */
export const FlipCard = withClick(FlipCardBase);

    Update the import paths to match your project setup.

    Props

    PropTypeDefaultDescription
    variant"front" OR "back""front"Indicates whether it's the front or back of the card.
    widthstring-Width of the card.
    heightstring-Height of the card.
    frontImagestring-URL of the front image of the card.
    widthstring-URL of the back image of the card.

    Notes

    • The literal and graphical information presented on this page about Magic: The Gathering, including card images and mana symbols, is copyright Wizards of the Coast, LLC. bubba/ui is not produced by or endorsed by Wizards of the Coast.

    Credits