Reveal Card
A dynamic card component with layered images and hover animations.
Reveal Cards
Hover to reveal the superhero within.



Installation Guide
1
Copy Component Code
Reveal Card.tsx
TypeScript1"use client";
2import React, { useEffect, useRef, useState } from "react";
3import Image from "next/image";
4
5interface CardProps {
6 coverImage: string;
7 titleImage: string;
8 characterImage: string;
9
10 width?: number;
11 height?: number;
12 backgroundColor?: string;
13 borderColor?: string;
14 hoverRotation?: number;
15 titleTranslateY?: number;
16 characterTranslateY?: number;
17 characterTranslateZ?: number;
18 alt?: {
19 cover?: string;
20 title?: string;
21 character?: string;
22 };
23 gradientColors?: {
24 top?: string;
25 bottom?: string;
26 };
27 animation?: {
28 duration?: number;
29 delay?: number;
30 };
31 shadow?: string;
32 priority?: boolean;
33 threshold?: number;
34}
35
36const RevealCard: React.FC<CardProps> = ({
37 coverImage,
38 titleImage,
39 characterImage,
40 width = 266,
41 height = 400,
42 backgroundColor = "#192740",
43 borderColor = "#ddd",
44 hoverRotation = 25,
45 titleTranslateY = -50,
46 characterTranslateY = -15,
47 characterTranslateZ = 100,
48 alt = {
49 cover: "Cover Image",
50 title: "Title",
51 character: "Character",
52 },
53 animation = {
54 duration: 500,
55 delay: 0,
56 },
57 shadow = "2px 35px 32px -8px rgba(0,0,0,0.75)",
58 priority = false,
59 threshold = 0.3,
60}) => {
61 const cardRef = useRef<HTMLDivElement>(null);
62 const [isVisible, setIsVisible] = useState(false);
63 const [isMobile, setIsMobile] = useState(false);
64 const [isRevealed, setIsRevealed] = useState(false);
65 const [hasBeenRevealed, setHasBeenRevealed] = useState(false);
66
67 useEffect(() => {
68 const checkMobile = () => {
69 setIsMobile(window.innerWidth < 768);
70 };
71 checkMobile();
72 window.addEventListener("resize", checkMobile);
73
74 const currentRef = cardRef.current;
75
76 const observer = new IntersectionObserver(
77 ([entry]) => {
78 if (entry.isIntersecting) {
79 setIsVisible(true);
80 if (!hasBeenRevealed) {
81 setIsRevealed(true);
82 setHasBeenRevealed(true);
83 }
84 } else {
85 setIsVisible(false);
86 }
87 },
88 { threshold }
89 );
90
91 if (currentRef) {
92 observer.observe(currentRef);
93 }
94
95 return () => {
96 window.removeEventListener("resize", checkMobile);
97 if (currentRef) observer.unobserve(currentRef);
98 };
99 }, [threshold, hasBeenRevealed]);
100
101 const handleCardClick = () => {
102 setIsRevealed(!isRevealed);
103 setHasBeenRevealed(true);
104 };
105
106 const animationStyle = {
107 transitionDuration: `${animation.duration}ms`,
108 transitionDelay: `${animation.delay}ms`,
109 };
110
111 const shouldReveal = isMobile
112 ? isRevealed
113 : isRevealed || (!hasBeenRevealed && isVisible);
114
115 const mobileRevealClass =
116 isMobile && shouldReveal
117 ? "[transform:perspective(900px)_translateY(-5%)_rotateX(25deg)_translateZ(0)] shadow-xl"
118 : "";
119
120 const characterRevealClass =
121 isMobile && shouldReveal
122 ? "opacity-100 [transform:translate3d(0,-25%,100px)]"
123 : "";
124
125 const titleRevealClass =
126 isMobile && shouldReveal ? "[transform:translate3d(0,-50px,100px)]" : "";
127
128 const desktopHoverClass = !isMobile
129 ? "group-hover:[transform:perspective(900px)_translateY(-5%)_rotateX(25deg)_translateZ(0)] group-hover:shadow-[2px_35px_32px_-8px_rgba(0,0,0,0.75)]"
130 : "";
131
132 const characterHoverClass = !isMobile
133 ? "group-hover:opacity-100 group-hover:[transform:translate3d(0,-25%,100px)]"
134 : "";
135
136 const titleHoverClass = !isMobile
137 ? "group-hover:[transform:translate3d(0,-50px,100px)]"
138 : "";
139
140 return (
141 <div
142 ref={cardRef}
143 className="group relative flex justify-center items-end no-underline perspective-[2500px] cursor-pointer"
144 style={{
145 width: `${width}px`,
146 height: `${height}px`,
147 backgroundColor,
148 borderColor,
149 padding: "0 36px",
150 margin: "0 10px",
151 border: `1px solid ${borderColor}`,
152 }}
153 onClick={handleCardClick}
154 >
155 <div className="absolute inset-0 overflow-hidden z-0">
156 <div
157 className={`absolute inset-0 transition-all duration-500 ${desktopHoverClass} ${mobileRevealClass}`}
158 style={
159 {
160 ...animationStyle,
161 "--hover-rotation": `${hoverRotation}deg`,
162 "--hover-shadow": shadow,
163 } as React.CSSProperties
164 }
165 >
166 <Image
167 src={coverImage}
168 alt={alt.cover || "Cover Image"}
169 fill
170 className="object-cover"
171 loading={priority ? "eager" : "lazy"}
172 priority={priority}
173 />
174
175 <div
176 className="absolute bottom-0 left-0 w-full h-[40px] bg-gradient-to-b from-transparent to-[rgba(12,13,19,0.3)]"
177 style={animationStyle}
178 ></div>
179 </div>
180 </div>
181
182 <div className="absolute inset-0 z-10 pointer-events-none">
183 <Image
184 src={characterImage}
185 alt={alt.character || "Character"}
186 fill
187 className={`object-cover opacity-0 transition-all duration-500 ${characterHoverClass} ${
188 shouldReveal ? characterRevealClass : ""
189 }`}
190 style={
191 {
192 ...animationStyle,
193 "--character-translate-y": `${characterTranslateY}%`,
194 "--character-translate-z": `${characterTranslateZ}px`,
195 } as React.CSSProperties
196 }
197 loading={priority ? "eager" : "lazy"}
198 priority={priority}
199 />
200 </div>
201
202 <div className="relative z-20 w-full">
203 <Image
204 src={titleImage}
205 alt={alt.title || "Title"}
206 width={500}
207 height={500}
208 className={`w-full transition-transform duration-500 ${titleHoverClass} ${
209 shouldReveal ? titleRevealClass : ""
210 }`}
211 style={
212 {
213 ...animationStyle,
214 "--title-translate-y": `${titleTranslateY}px`,
215 "--title-translate-z": `${characterTranslateZ}px`,
216 } as React.CSSProperties
217 }
218 loading={priority ? "eager" : "lazy"}
219 priority={priority}
220 />
221 </div>
222
223 {isMobile && (
224 <div className="absolute bottom-2 right-2 z-30 w-8 h-8 rounded-full bg-white/20 flex items-center justify-center transition-opacity duration-300">
225 <span className="text-white text-xs">{isRevealed ? "×" : "+"}</span>
226 </div>
227 )}
228 </div>
229 );
230};
231
232export default RevealCard;
233
2
Final Steps
Update Import Paths
Make sure to update the import paths in the component code to match your project structure. For example, change @/components/ui/button
to match your UI components location.
Props
Name | Type | Default | Description |
---|---|---|---|
coverImage | string | "" | URL for the cover image. |
titleImage | string | "" | URL for the title image. |
characterImage | string | "" | URL for the character image. |
width | number | 266 | Width of the card in pixels. |
height | number | 400 | Height of the card in pixels. |
backgroundColor | string | "#192740" | Background color of the card. |
borderColor | string | "#ddd" | Border color of the card. |
hoverRotation | number | 25 | Rotation angle on hover. |
titleTranslateY | number | -50 | Y-axis translation for title image on hover. |
characterTranslateY | number | -30 | Y-axis translation for character image on hover. |
characterTranslateZ | number | 100 | Z-axis translation for character image on hover. |
alt | { cover?: string; title?: string; character?: string; } | { cover: "Cover Image", title: "Title", character: "Character" } | Alternate text for the images. |
gradientColors | { top?: string; bottom?: string; } | { top: "rgba(12,13,19,1)", bottom: "rgba(12,13,19,1)" } | Gradient overlay colors for the cover image. |
animation | { duration?: number; delay?: number; } | { duration: 500, delay: 0 } | Animation duration and delay in milliseconds. |
shadow | string | "2px 35px 32px -8px rgba(0,0,0,0.75)" | Shadow effect applied on hover. |
priority | boolean | false | If true, images are prioritized for loading (eager loading). |
Examples





