Example
Click card to flip it
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
Prop | Type | Default | Description |
---|---|---|---|
variant | "front" OR "back" | "front" | Indicates whether it's the front or back of the card. |
width | string | - | Width of the card. |
height | string | - | Height of the card. |
frontImage | string | - | URL of the front image of the card. |
width | string | - | 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
- Inspired by the card effects found on https://magic.wizards.com/en/products/modern-horizons-3