Flip Card

A card component that animates flipping between front and back, ideal for info cards, games, or interactive content.

Front SideThis is the front side of the card.
Back SideThis is the back side of the card.

Install the following dependencies:

pnpm install motion

Copy and paste the following code into your project:

1import React, { useState, useImperativeHandle, forwardRef } from "react";
2import { Easing, motion } from "motion/react";
3import { cn } from "@/lib/utils";
4
5interface FlipCardProps {
6 frontContent: React.ReactNode;
7 backContent: React.ReactNode;
8 width?: string;
9 height?: string;
10 flipDirection?: "horizontal" | "vertical";
11 className?: string;
12 disabled?: boolean;
13 initialFlipped?: boolean;
14 onFlip?: (isFlipped: boolean) => void;
15 flipTrigger?: "click" | "hover" | "manual";
16 duration?: number;
17 easing?: Easing | Easing[];
18}
19
20export interface FlipCardHandle {
21 flip: () => void;
22 setFlipped: (flipped: boolean) => void;
23 isFlipped: boolean;
24}
25
26const FlipCard = forwardRef<FlipCardHandle, FlipCardProps>(
27 (
28 {
29 frontContent,
30 backContent,
31 width = "w-80",
32 height = "h-96",
33 flipDirection = "horizontal",
34 className,
35 disabled = false,
36 initialFlipped = false,
37 onFlip,
38 flipTrigger = "click",
39 duration = 0.6,
40 easing = "easeInOut",
41 },
42 ref
43 ) => {
44 const [isFlipped, setIsFlipped] = useState(initialFlipped);
45
46 const handleFlip = () => {
47 if (disabled || flipTrigger === "manual") return;
48 const newFlippedState = !isFlipped;
49 setIsFlipped(newFlippedState);
50 onFlip?.(newFlippedState);
51 };
52
53 const handleMouseEnter = () => {
54 if (flipTrigger === "hover" && !disabled) {
55 setIsFlipped(true);
56 onFlip?.(true);
57 }
58 };
59
60 const handleMouseLeave = () => {
61 if (flipTrigger === "hover" && !disabled) {
62 setIsFlipped(false);
63 onFlip?.(false);
64 }
65 };
66
67 useImperativeHandle(
68 ref,
69 () => ({
70 flip: () => {
71 const newFlippedState = !isFlipped;
72 setIsFlipped(newFlippedState);
73 onFlip?.(newFlippedState);
74 },
75 setFlipped: (flipped: boolean) => {
76 setIsFlipped(flipped);
77 onFlip?.(flipped);
78 },
79 get isFlipped() {
80 return isFlipped;
81 },
82 }),
83 [isFlipped, onFlip]
84 );
85
86 const flipVariants = {
87 horizontal: {
88 front: {
89 rotateY: isFlipped ? 180 : 0,
90 },
91 back: {
92 rotateY: isFlipped ? 0 : -180,
93 },
94 },
95 vertical: {
96 front: {
97 rotateX: isFlipped ? 180 : 0,
98 },
99 back: {
100 rotateX: isFlipped ? 0 : -180,
101 },
102 },
103 };
104
105 return (
106 <div
107 className={cn(
108 width,
109 height,
110 "perspective-1000",
111 !disabled && flipTrigger === "click" && "cursor-pointer",
112 className
113 )}
114 onClick={flipTrigger === "click" ? handleFlip : undefined}
115 onMouseEnter={handleMouseEnter}
116 onMouseLeave={handleMouseLeave}
117 >
118 <motion.div
119 className="relative w-full h-full"
120 style={{ transformStyle: "preserve-3d" }}
121 >
122 <motion.div
123 className="absolute inset-0"
124 variants={flipVariants[flipDirection]}
125 animate="front"
126 transition={{ duration, ease: easing }}
127 style={{ backfaceVisibility: "hidden" }}
128 >
129 {frontContent}
130 </motion.div>
131
132 <motion.div
133 className="absolute inset-0"
134 variants={flipVariants[flipDirection]}
135 animate="back"
136 transition={{ duration, ease: easing }}
137 style={{
138 backfaceVisibility: "hidden",
139 transform:
140 flipDirection === "horizontal"
141 ? "rotateY(-180deg)"
142 : "rotateX(-180deg)",
143 }}
144 >
145 {backContent}
146 </motion.div>
147 </motion.div>
148 </div>
149 );
150 }
151);
152
153export { FlipCard };

Usage

1import { FlipCard } from "@/components/ui/flip-card";
2import React, { useRef } from "react";
3
4export function Example() {
5 const flipCardRef = useRef(null);
6
7 return (
8 <>
9 <FlipCard
10 ref={flipCardRef}
11 width="w-64"
12 height="h-80"
13 frontContent={
14 <div className="flex flex-col items-center justify-center h-full w-full bg-primary text-primary-foreground rounded-lg p-4">
15 <span className="text-lg font-semibold">Front Side</span>
16 <span className="text-sm mt-2">This is the front side of the card.</span>
17 </div>
18 }
19 backContent={
20 <div className="flex flex-col items-center justify-center h-full w-full bg-secondary text-secondary-foreground rounded-lg p-4">
21 <span className="text-lg font-semibold">Back Side</span>
22 <span className="text-sm mt-2">This is the back side of the card.</span>
23 </div>
24 }
25 flipDirection="horizontal"
26 flipTrigger="click"
27 />
28 </>
29 );
30}

Props

PropTypeDescriptionDefault
frontContentReactNodeContent to be displayed on the front of the card.-
backContentReactNodeContent to be displayed on the back of the card.-
widthstringCard width (Tailwind class)."w-80"
heightstringCard height (Tailwind class)."h-96"
flipDirection"horizontal" | "vertical"Flip direction (horizontal/vertical)."horizontal"
classNamestringExtra Tailwind/customization classes.-
disabledbooleanDisables the card.false
initialFlippedbooleanDetermines if the card is initially flipped.false
onFlip(isFlipped: boolean) => voidCalled when the card is flipped.-
flipTrigger"click" | "hover" | "manual"Flip trigger (click, hover, manual)."click"
durationnumberAnimation duration (seconds).0.6
easingEasing | Easing[]Animation curve."easeInOut"