Introducing Nuvyx UI v1.0.0

Reveal Card

A dynamic card component with layered images and hover animations.

Reveal Cards

Hover to reveal the superhero within.

Cover Image
Character
Title

Installation Guide

1

Copy Component Code

Reveal Card.tsx
TypeScript
1"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

NameTypeDefaultDescription
coverImagestring""URL for the cover image.
titleImagestring""URL for the title image.
characterImagestring""URL for the character image.
widthnumber266Width of the card in pixels.
heightnumber400Height of the card in pixels.
backgroundColorstring"#192740"Background color of the card.
borderColorstring"#ddd"Border color of the card.
hoverRotationnumber25Rotation angle on hover.
titleTranslateYnumber-50Y-axis translation for title image on hover.
characterTranslateYnumber-30Y-axis translation for character image on hover.
characterTranslateZnumber100Z-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.
shadowstring"2px 35px 32px -8px rgba(0,0,0,0.75)"Shadow effect applied on hover.
prioritybooleanfalseIf true, images are prioritized for loading (eager loading).

Examples

Cover Image
Character
Title
Cover Image
Character
Title