Perspective Card

A component for displaying cards in a perspective view.

Default
Default perspective card
Card Title

Card Title

Card description

1"use client";
2
3import { useState, useRef } from "react";
4import { cn } from "@/lib/utils";
5
6interface PerspectiveCardProps {
7 title: string;
8 description: string;
9 image: string;
10 className?: string;
11 cardHeaderClassName?: string;
12 cardTitleClassName?: string;
13 cardContentClassName?: string;
14 imageContainerClassName?: string;
15 descriptionClassName?: string;
16 overlayClassName?: string;
17 rotationIntensity?: number;
18 disablePerspective?: boolean;
19 cardStyle?: React.CSSProperties;
20 titleColor?: string;
21 descriptionColor?: string;
22 bgColor?: string;
23 borderColor?: string;
24}
25
26export function PerspectiveCard({
27 title,
28 description,
29 image,
30 className,
31 cardHeaderClassName,
32 cardTitleClassName,
33 cardContentClassName,
34 imageContainerClassName,
35 descriptionClassName,
36 overlayClassName,
37 rotationIntensity = 10,
38 disablePerspective = false,
39 cardStyle,
40 titleColor,
41 descriptionColor,
42 bgColor,
43 borderColor,
44}: PerspectiveCardProps) {
45 const [rotation, setRotation] = useState({ x: 0, y: 0 });
46 const [imageError, setImageError] = useState(false);
47 const cardRef = useRef<HTMLDivElement>(null);
48
49 const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
50 if (!cardRef.current || disablePerspective) return;
51
52 const rect = cardRef.current.getBoundingClientRect();
53 const x = e.clientX - rect.left;
54 const y = e.clientY - rect.top;
55
56 const centerX = rect.width / 2;
57 const centerY = rect.height / 2;
58
59 const rotateX = ((y - centerY) / centerY) * -rotationIntensity;
60 const rotateY = ((x - centerX) / centerX) * rotationIntensity;
61
62 setRotation({ x: rotateX, y: rotateY });
63 };
64
65 const handleMouseLeave = () => {
66 setRotation({ x: 0, y: 0 });
67 };
68
69 const handleImageError = () => {
70 setImageError(true);
71 };
72
73 // Combine inline styles
74 const computedCardStyle = {
75 ...cardStyle,
76 backgroundColor: bgColor,
77 borderColor: borderColor,
78 transform: disablePerspective
79 ? "none"
80 : `perspective(1000px) rotateX(${rotation.x}deg) rotateY(${rotation.y}deg)`,
81 };
82
83 return (
84 <div
85 ref={cardRef}
86 className={cn(
87 // Base styles
88 "rounded-lg border shadow-sm",
89 "group relative overflow-hidden transition-all duration-300 ease-out hover:shadow-xl",
90 // Custom classes should come last to override base styles
91 className
92 )}
93 onMouseMove={handleMouseMove}
94 onMouseLeave={handleMouseLeave}
95 style={computedCardStyle}
96 >
97 <div className={cn("h-48 overflow-hidden", imageContainerClassName)}>
98 {!imageError ? (
99 <img
100 src={image}
101 alt={title}
102 className="w-full h-full object-cover transition-transform duration-300 ease-out group-hover:scale-110"
103 onError={handleImageError}
104 crossOrigin="anonymous"
105 />
106 ) : (
107 <div className="w-full h-full flex items-center justify-center bg-gray-100 text-gray-400 text-sm">
108 Image not available
109 </div>
110 )}
111 </div>
112 <div
113 className={cn(
114 "absolute inset-0 bg-gradient-to-b from-black/50 to-black/0 opacity-0 transition-opacity group-hover:opacity-100",
115 overlayClassName
116 )}
117 />
118 <div
119 className={cn(
120 "flex flex-col space-y-1.5 p-6 relative z-10",
121 cardHeaderClassName
122 )}
123 >
124 <h3
125 className={cn(
126 "font-semibold leading-none tracking-tight transition-colors group-hover:text-white text-lg",
127 cardTitleClassName
128 )}
129 style={{ color: titleColor }}
130 >
131 {title}
132 </h3>
133 </div>
134 <div className={cn("p-6 pt-0 relative z-10", cardContentClassName)}>
135 <p
136 className={cn(
137 "text-sm transition-colors group-hover:text-white/80",
138 descriptionClassName
139 )}
140 style={{ color: descriptionColor }}
141 >
142 {description}
143 </p>
144 </div>
145 </div>
146 );
147}
Custom Styling
Custom styling for the card
Custom Styled Card

Custom Styled Card

This has custom styles for various parts

1"use client";
2
3import { useState, useRef } from "react";
4import { cn } from "@/lib/utils";
5
6interface PerspectiveCardProps {
7 title: string;
8 description: string;
9 image: string;
10 className?: string;
11 cardHeaderClassName?: string;
12 cardTitleClassName?: string;
13 cardContentClassName?: string;
14 imageContainerClassName?: string;
15 descriptionClassName?: string;
16 overlayClassName?: string;
17 rotationIntensity?: number;
18 disablePerspective?: boolean;
19 cardStyle?: React.CSSProperties;
20 titleColor?: string;
21 descriptionColor?: string;
22 bgColor?: string;
23 borderColor?: string;
24}
25
26export function PerspectiveCard({
27 title,
28 description,
29 image,
30 className,
31 cardHeaderClassName,
32 cardTitleClassName,
33 cardContentClassName,
34 imageContainerClassName,
35 descriptionClassName,
36 overlayClassName,
37 rotationIntensity = 10,
38 disablePerspective = false,
39 cardStyle,
40 titleColor,
41 descriptionColor,
42 bgColor,
43 borderColor,
44}: PerspectiveCardProps) {
45 const [rotation, setRotation] = useState({ x: 0, y: 0 });
46 const [imageError, setImageError] = useState(false);
47 const cardRef = useRef<HTMLDivElement>(null);
48
49 const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
50 if (!cardRef.current || disablePerspective) return;
51
52 const rect = cardRef.current.getBoundingClientRect();
53 const x = e.clientX - rect.left;
54 const y = e.clientY - rect.top;
55
56 const centerX = rect.width / 2;
57 const centerY = rect.height / 2;
58
59 const rotateX = ((y - centerY) / centerY) * -rotationIntensity;
60 const rotateY = ((x - centerX) / centerX) * rotationIntensity;
61
62 setRotation({ x: rotateX, y: rotateY });
63 };
64
65 const handleMouseLeave = () => {
66 setRotation({ x: 0, y: 0 });
67 };
68
69 const handleImageError = () => {
70 setImageError(true);
71 };
72
73 // Combine inline styles
74 const computedCardStyle = {
75 ...cardStyle,
76 backgroundColor: bgColor,
77 borderColor: borderColor,
78 transform: disablePerspective
79 ? "none"
80 : `perspective(1000px) rotateX(${rotation.x}deg) rotateY(${rotation.y}deg)`,
81 };
82
83 return (
84 <div
85 ref={cardRef}
86 className={cn(
87 // Base styles
88 "rounded-lg border shadow-sm",
89 "group relative overflow-hidden transition-all duration-300 ease-out hover:shadow-xl",
90 // Custom classes should come last to override base styles
91 className
92 )}
93 onMouseMove={handleMouseMove}
94 onMouseLeave={handleMouseLeave}
95 style={computedCardStyle}
96 >
97 <div className={cn("h-48 overflow-hidden", imageContainerClassName)}>
98 {!imageError ? (
99 <img
100 src={image}
101 alt={title}
102 className="w-full h-full object-cover transition-transform duration-300 ease-out group-hover:scale-110"
103 onError={handleImageError}
104 crossOrigin="anonymous"
105 />
106 ) : (
107 <div className="w-full h-full flex items-center justify-center bg-gray-100 text-gray-400 text-sm">
108 Image not available
109 </div>
110 )}
111 </div>
112 <div
113 className={cn(
114 "absolute inset-0 bg-gradient-to-b from-black/50 to-black/0 opacity-0 transition-opacity group-hover:opacity-100",
115 overlayClassName
116 )}
117 />
118 <div
119 className={cn(
120 "flex flex-col space-y-1.5 p-6 relative z-10",
121 cardHeaderClassName
122 )}
123 >
124 <h3
125 className={cn(
126 "font-semibold leading-none tracking-tight transition-colors group-hover:text-white text-lg",
127 cardTitleClassName
128 )}
129 style={{ color: titleColor }}
130 >
131 {title}
132 </h3>
133 </div>
134 <div className={cn("p-6 pt-0 relative z-10", cardContentClassName)}>
135 <p
136 className={cn(
137 "text-sm transition-colors group-hover:text-white/80",
138 descriptionClassName
139 )}
140 style={{ color: descriptionColor }}
141 >
142 {description}
143 </p>
144 </div>
145 </div>
146 );
147}
Custom Colours
Custom colours for the card using direct color props
Custom Colours

Custom Colours

Using direct color props

1"use client";
2
3import { useState, useRef } from "react";
4import { cn } from "@/lib/utils";
5
6interface PerspectiveCardProps {
7 title: string;
8 description: string;
9 image: string;
10 className?: string;
11 cardHeaderClassName?: string;
12 cardTitleClassName?: string;
13 cardContentClassName?: string;
14 imageContainerClassName?: string;
15 descriptionClassName?: string;
16 overlayClassName?: string;
17 rotationIntensity?: number;
18 disablePerspective?: boolean;
19 cardStyle?: React.CSSProperties;
20 titleColor?: string;
21 descriptionColor?: string;
22 bgColor?: string;
23 borderColor?: string;
24}
25
26export function PerspectiveCard({
27 title,
28 description,
29 image,
30 className,
31 cardHeaderClassName,
32 cardTitleClassName,
33 cardContentClassName,
34 imageContainerClassName,
35 descriptionClassName,
36 overlayClassName,
37 rotationIntensity = 10,
38 disablePerspective = false,
39 cardStyle,
40 titleColor,
41 descriptionColor,
42 bgColor,
43 borderColor,
44}: PerspectiveCardProps) {
45 const [rotation, setRotation] = useState({ x: 0, y: 0 });
46 const [imageError, setImageError] = useState(false);
47 const cardRef = useRef<HTMLDivElement>(null);
48
49 const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
50 if (!cardRef.current || disablePerspective) return;
51
52 const rect = cardRef.current.getBoundingClientRect();
53 const x = e.clientX - rect.left;
54 const y = e.clientY - rect.top;
55
56 const centerX = rect.width / 2;
57 const centerY = rect.height / 2;
58
59 const rotateX = ((y - centerY) / centerY) * -rotationIntensity;
60 const rotateY = ((x - centerX) / centerX) * rotationIntensity;
61
62 setRotation({ x: rotateX, y: rotateY });
63 };
64
65 const handleMouseLeave = () => {
66 setRotation({ x: 0, y: 0 });
67 };
68
69 const handleImageError = () => {
70 setImageError(true);
71 };
72
73 // Combine inline styles
74 const computedCardStyle = {
75 ...cardStyle,
76 backgroundColor: bgColor,
77 borderColor: borderColor,
78 transform: disablePerspective
79 ? "none"
80 : `perspective(1000px) rotateX(${rotation.x}deg) rotateY(${rotation.y}deg)`,
81 };
82
83 return (
84 <div
85 ref={cardRef}
86 className={cn(
87 // Base styles
88 "rounded-lg border shadow-sm",
89 "group relative overflow-hidden transition-all duration-300 ease-out hover:shadow-xl",
90 // Custom classes should come last to override base styles
91 className
92 )}
93 onMouseMove={handleMouseMove}
94 onMouseLeave={handleMouseLeave}
95 style={computedCardStyle}
96 >
97 <div className={cn("h-48 overflow-hidden", imageContainerClassName)}>
98 {!imageError ? (
99 <img
100 src={image}
101 alt={title}
102 className="w-full h-full object-cover transition-transform duration-300 ease-out group-hover:scale-110"
103 onError={handleImageError}
104 crossOrigin="anonymous"
105 />
106 ) : (
107 <div className="w-full h-full flex items-center justify-center bg-gray-100 text-gray-400 text-sm">
108 Image not available
109 </div>
110 )}
111 </div>
112 <div
113 className={cn(
114 "absolute inset-0 bg-gradient-to-b from-black/50 to-black/0 opacity-0 transition-opacity group-hover:opacity-100",
115 overlayClassName
116 )}
117 />
118 <div
119 className={cn(
120 "flex flex-col space-y-1.5 p-6 relative z-10",
121 cardHeaderClassName
122 )}
123 >
124 <h3
125 className={cn(
126 "font-semibold leading-none tracking-tight transition-colors group-hover:text-white text-lg",
127 cardTitleClassName
128 )}
129 style={{ color: titleColor }}
130 >
131 {title}
132 </h3>
133 </div>
134 <div className={cn("p-6 pt-0 relative z-10", cardContentClassName)}>
135 <p
136 className={cn(
137 "text-sm transition-colors group-hover:text-white/80",
138 descriptionClassName
139 )}
140 style={{ color: descriptionColor }}
141 >
142 {description}
143 </p>
144 </div>
145 </div>
146 );
147}
Tailwind Color Classes
Using Tailwind color classes with tailwind-merge
Tailwind Colors

Tailwind Colors

Using Tailwind color utilities

1"use client";
2
3import { useState, useRef } from "react";
4import { cn } from "@/lib/utils";
5
6interface PerspectiveCardProps {
7 title: string;
8 description: string;
9 image: string;
10 className?: string;
11 cardHeaderClassName?: string;
12 cardTitleClassName?: string;
13 cardContentClassName?: string;
14 imageContainerClassName?: string;
15 descriptionClassName?: string;
16 overlayClassName?: string;
17 rotationIntensity?: number;
18 disablePerspective?: boolean;
19 cardStyle?: React.CSSProperties;
20 titleColor?: string;
21 descriptionColor?: string;
22 bgColor?: string;
23 borderColor?: string;
24}
25
26export function PerspectiveCard({
27 title,
28 description,
29 image,
30 className,
31 cardHeaderClassName,
32 cardTitleClassName,
33 cardContentClassName,
34 imageContainerClassName,
35 descriptionClassName,
36 overlayClassName,
37 rotationIntensity = 10,
38 disablePerspective = false,
39 cardStyle,
40 titleColor,
41 descriptionColor,
42 bgColor,
43 borderColor,
44}: PerspectiveCardProps) {
45 const [rotation, setRotation] = useState({ x: 0, y: 0 });
46 const [imageError, setImageError] = useState(false);
47 const cardRef = useRef<HTMLDivElement>(null);
48
49 const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
50 if (!cardRef.current || disablePerspective) return;
51
52 const rect = cardRef.current.getBoundingClientRect();
53 const x = e.clientX - rect.left;
54 const y = e.clientY - rect.top;
55
56 const centerX = rect.width / 2;
57 const centerY = rect.height / 2;
58
59 const rotateX = ((y - centerY) / centerY) * -rotationIntensity;
60 const rotateY = ((x - centerX) / centerX) * rotationIntensity;
61
62 setRotation({ x: rotateX, y: rotateY });
63 };
64
65 const handleMouseLeave = () => {
66 setRotation({ x: 0, y: 0 });
67 };
68
69 const handleImageError = () => {
70 setImageError(true);
71 };
72
73 // Combine inline styles
74 const computedCardStyle = {
75 ...cardStyle,
76 backgroundColor: bgColor,
77 borderColor: borderColor,
78 transform: disablePerspective
79 ? "none"
80 : `perspective(1000px) rotateX(${rotation.x}deg) rotateY(${rotation.y}deg)`,
81 };
82
83 return (
84 <div
85 ref={cardRef}
86 className={cn(
87 // Base styles
88 "rounded-lg border shadow-sm",
89 "group relative overflow-hidden transition-all duration-300 ease-out hover:shadow-xl",
90 // Custom classes should come last to override base styles
91 className
92 )}
93 onMouseMove={handleMouseMove}
94 onMouseLeave={handleMouseLeave}
95 style={computedCardStyle}
96 >
97 <div className={cn("h-48 overflow-hidden", imageContainerClassName)}>
98 {!imageError ? (
99 <img
100 src={image}
101 alt={title}
102 className="w-full h-full object-cover transition-transform duration-300 ease-out group-hover:scale-110"
103 onError={handleImageError}
104 crossOrigin="anonymous"
105 />
106 ) : (
107 <div className="w-full h-full flex items-center justify-center bg-gray-100 text-gray-400 text-sm">
108 Image not available
109 </div>
110 )}
111 </div>
112 <div
113 className={cn(
114 "absolute inset-0 bg-gradient-to-b from-black/50 to-black/0 opacity-0 transition-opacity group-hover:opacity-100",
115 overlayClassName
116 )}
117 />
118 <div
119 className={cn(
120 "flex flex-col space-y-1.5 p-6 relative z-10",
121 cardHeaderClassName
122 )}
123 >
124 <h3
125 className={cn(
126 "font-semibold leading-none tracking-tight transition-colors group-hover:text-white text-lg",
127 cardTitleClassName
128 )}
129 style={{ color: titleColor }}
130 >
131 {title}
132 </h3>
133 </div>
134 <div className={cn("p-6 pt-0 relative z-10", cardContentClassName)}>
135 <p
136 className={cn(
137 "text-sm transition-colors group-hover:text-white/80",
138 descriptionClassName
139 )}
140 style={{ color: descriptionColor }}
141 >
142 {description}
143 </p>
144 </div>
145 </div>
146 );
147}
Multiple Styling Options
Different ways to style the PerspectiveCard
Combined Styling

Combined Styling

Mix of Tailwind and custom styles

1"use client";
2
3import { useState, useRef } from "react";
4import { cn } from "@/lib/utils";
5
6interface PerspectiveCardProps {
7 title: string;
8 description: string;
9 image: string;
10 className?: string;
11 cardHeaderClassName?: string;
12 cardTitleClassName?: string;
13 cardContentClassName?: string;
14 imageContainerClassName?: string;
15 descriptionClassName?: string;
16 overlayClassName?: string;
17 rotationIntensity?: number;
18 disablePerspective?: boolean;
19 cardStyle?: React.CSSProperties;
20 titleColor?: string;
21 descriptionColor?: string;
22 bgColor?: string;
23 borderColor?: string;
24}
25
26export function PerspectiveCard({
27 title,
28 description,
29 image,
30 className,
31 cardHeaderClassName,
32 cardTitleClassName,
33 cardContentClassName,
34 imageContainerClassName,
35 descriptionClassName,
36 overlayClassName,
37 rotationIntensity = 10,
38 disablePerspective = false,
39 cardStyle,
40 titleColor,
41 descriptionColor,
42 bgColor,
43 borderColor,
44}: PerspectiveCardProps) {
45 const [rotation, setRotation] = useState({ x: 0, y: 0 });
46 const [imageError, setImageError] = useState(false);
47 const cardRef = useRef<HTMLDivElement>(null);
48
49 const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
50 if (!cardRef.current || disablePerspective) return;
51
52 const rect = cardRef.current.getBoundingClientRect();
53 const x = e.clientX - rect.left;
54 const y = e.clientY - rect.top;
55
56 const centerX = rect.width / 2;
57 const centerY = rect.height / 2;
58
59 const rotateX = ((y - centerY) / centerY) * -rotationIntensity;
60 const rotateY = ((x - centerX) / centerX) * rotationIntensity;
61
62 setRotation({ x: rotateX, y: rotateY });
63 };
64
65 const handleMouseLeave = () => {
66 setRotation({ x: 0, y: 0 });
67 };
68
69 const handleImageError = () => {
70 setImageError(true);
71 };
72
73 // Combine inline styles
74 const computedCardStyle = {
75 ...cardStyle,
76 backgroundColor: bgColor,
77 borderColor: borderColor,
78 transform: disablePerspective
79 ? "none"
80 : `perspective(1000px) rotateX(${rotation.x}deg) rotateY(${rotation.y}deg)`,
81 };
82
83 return (
84 <div
85 ref={cardRef}
86 className={cn(
87 // Base styles
88 "rounded-lg border shadow-sm",
89 "group relative overflow-hidden transition-all duration-300 ease-out hover:shadow-xl",
90 // Custom classes should come last to override base styles
91 className
92 )}
93 onMouseMove={handleMouseMove}
94 onMouseLeave={handleMouseLeave}
95 style={computedCardStyle}
96 >
97 <div className={cn("h-48 overflow-hidden", imageContainerClassName)}>
98 {!imageError ? (
99 <img
100 src={image}
101 alt={title}
102 className="w-full h-full object-cover transition-transform duration-300 ease-out group-hover:scale-110"
103 onError={handleImageError}
104 crossOrigin="anonymous"
105 />
106 ) : (
107 <div className="w-full h-full flex items-center justify-center bg-gray-100 text-gray-400 text-sm">
108 Image not available
109 </div>
110 )}
111 </div>
112 <div
113 className={cn(
114 "absolute inset-0 bg-gradient-to-b from-black/50 to-black/0 opacity-0 transition-opacity group-hover:opacity-100",
115 overlayClassName
116 )}
117 />
118 <div
119 className={cn(
120 "flex flex-col space-y-1.5 p-6 relative z-10",
121 cardHeaderClassName
122 )}
123 >
124 <h3
125 className={cn(
126 "font-semibold leading-none tracking-tight transition-colors group-hover:text-white text-lg",
127 cardTitleClassName
128 )}
129 style={{ color: titleColor }}
130 >
131 {title}
132 </h3>
133 </div>
134 <div className={cn("p-6 pt-0 relative z-10", cardContentClassName)}>
135 <p
136 className={cn(
137 "text-sm transition-colors group-hover:text-white/80",
138 descriptionClassName
139 )}
140 style={{ color: descriptionColor }}
141 >
142 {description}
143 </p>
144 </div>
145 </div>
146 );
147}
Disable Perspective
Disable the perspective effect
No Perspective

No Perspective

This has the perspective effect disabled

1"use client";
2
3import { useState, useRef } from "react";
4import { cn } from "@/lib/utils";
5
6interface PerspectiveCardProps {
7 title: string;
8 description: string;
9 image: string;
10 className?: string;
11 cardHeaderClassName?: string;
12 cardTitleClassName?: string;
13 cardContentClassName?: string;
14 imageContainerClassName?: string;
15 descriptionClassName?: string;
16 overlayClassName?: string;
17 rotationIntensity?: number;
18 disablePerspective?: boolean;
19 cardStyle?: React.CSSProperties;
20 titleColor?: string;
21 descriptionColor?: string;
22 bgColor?: string;
23 borderColor?: string;
24}
25
26export function PerspectiveCard({
27 title,
28 description,
29 image,
30 className,
31 cardHeaderClassName,
32 cardTitleClassName,
33 cardContentClassName,
34 imageContainerClassName,
35 descriptionClassName,
36 overlayClassName,
37 rotationIntensity = 10,
38 disablePerspective = false,
39 cardStyle,
40 titleColor,
41 descriptionColor,
42 bgColor,
43 borderColor,
44}: PerspectiveCardProps) {
45 const [rotation, setRotation] = useState({ x: 0, y: 0 });
46 const [imageError, setImageError] = useState(false);
47 const cardRef = useRef<HTMLDivElement>(null);
48
49 const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
50 if (!cardRef.current || disablePerspective) return;
51
52 const rect = cardRef.current.getBoundingClientRect();
53 const x = e.clientX - rect.left;
54 const y = e.clientY - rect.top;
55
56 const centerX = rect.width / 2;
57 const centerY = rect.height / 2;
58
59 const rotateX = ((y - centerY) / centerY) * -rotationIntensity;
60 const rotateY = ((x - centerX) / centerX) * rotationIntensity;
61
62 setRotation({ x: rotateX, y: rotateY });
63 };
64
65 const handleMouseLeave = () => {
66 setRotation({ x: 0, y: 0 });
67 };
68
69 const handleImageError = () => {
70 setImageError(true);
71 };
72
73 // Combine inline styles
74 const computedCardStyle = {
75 ...cardStyle,
76 backgroundColor: bgColor,
77 borderColor: borderColor,
78 transform: disablePerspective
79 ? "none"
80 : `perspective(1000px) rotateX(${rotation.x}deg) rotateY(${rotation.y}deg)`,
81 };
82
83 return (
84 <div
85 ref={cardRef}
86 className={cn(
87 // Base styles
88 "rounded-lg border shadow-sm",
89 "group relative overflow-hidden transition-all duration-300 ease-out hover:shadow-xl",
90 // Custom classes should come last to override base styles
91 className
92 )}
93 onMouseMove={handleMouseMove}
94 onMouseLeave={handleMouseLeave}
95 style={computedCardStyle}
96 >
97 <div className={cn("h-48 overflow-hidden", imageContainerClassName)}>
98 {!imageError ? (
99 <img
100 src={image}
101 alt={title}
102 className="w-full h-full object-cover transition-transform duration-300 ease-out group-hover:scale-110"
103 onError={handleImageError}
104 crossOrigin="anonymous"
105 />
106 ) : (
107 <div className="w-full h-full flex items-center justify-center bg-gray-100 text-gray-400 text-sm">
108 Image not available
109 </div>
110 )}
111 </div>
112 <div
113 className={cn(
114 "absolute inset-0 bg-gradient-to-b from-black/50 to-black/0 opacity-0 transition-opacity group-hover:opacity-100",
115 overlayClassName
116 )}
117 />
118 <div
119 className={cn(
120 "flex flex-col space-y-1.5 p-6 relative z-10",
121 cardHeaderClassName
122 )}
123 >
124 <h3
125 className={cn(
126 "font-semibold leading-none tracking-tight transition-colors group-hover:text-white text-lg",
127 cardTitleClassName
128 )}
129 style={{ color: titleColor }}
130 >
131 {title}
132 </h3>
133 </div>
134 <div className={cn("p-6 pt-0 relative z-10", cardContentClassName)}>
135 <p
136 className={cn(
137 "text-sm transition-colors group-hover:text-white/80",
138 descriptionClassName
139 )}
140 style={{ color: descriptionColor }}
141 >
142 {description}
143 </p>
144 </div>
145 </div>
146 );
147}

Props

NameTypeDefaultDescription
titlestringRequiredTitle of the card
descriptionstringRequiredDescription of the card
imagestringRequiredURL of the card image (local or remote)
classNamestringTailwind classes for styling the card. These will properly override base styles using tailwind-merge.
cardStyleReact.CSSProperties{}Inline styles to apply directly to the card. Useful for custom shadows, animations, or complex styling.
bgColorstringDirect background color value (hex, rgb, etc.) for the card. Alternative to Tailwind bg-* classes.
borderColorstringDirect border color value (hex, rgb, etc.) for the card. Alternative to Tailwind border-* classes.
titleColorstringDirect text color value (hex, rgb, etc.) for the title. Alternative to Tailwind text-* classes.
descriptionColorstringDirect text color value (hex, rgb, etc.) for the description. Alternative to Tailwind text-* classes.
cardHeaderClassNamestringTailwind classes for styling the card header. These will properly override base styles using tailwind-merge.
cardTitleClassNamestringTailwind classes for styling the card title. These will properly override base styles using tailwind-merge.
cardContentClassNamestringTailwind classes for styling the card content. These will properly override base styles using tailwind-merge.
imageContainerClassNamestringTailwind classes for styling the image container. These will properly override base styles using tailwind-merge.
overlayClassNamestringTailwind classes for styling the gradient overlay. These will properly override base styles using tailwind-merge.
descriptionClassNamestringTailwind classes for styling the description. These will properly override base styles using tailwind-merge.
rotationIntensitynumber10How much the card rotates on hover (higher = more rotation)
disablePerspectivebooleanfalseDisables the 3D perspective effect