Docs
Go back
Component by
OV
OblivionMaster Vault
Platinum Pulse
•••• •••• •••• 8842
Member since2026
Expiry date12/28
Master Vault
Neon Nexus
•••• •••• •••• 4421
Member since2026
Expiry date12/28
Master Vault
Cyber Slate
•••• •••• •••• 0098
Member since2026
Expiry date12/28
TSX
three-d-smartcards
"use client"
import { motion, useMotionValue, useSpring, useTransform, AnimatePresence } from "framer-motion"
import { useState, useEffect, useRef } from "react"
import { cn } from "@/lib/utils"
interface CardData {
id: string
title: string
number: string
gradient: string
textColor: string
secondaryColor: string
}
interface ThreeDSmartCardsProps {
cards?: CardData[]
className?: string
autoPlay?: boolean
interval?: number
}
export const componentMeta = {
name: "3D Smartcards",
description: "A premium 3D stacking card animation with holographic effects and smooth flipping transitions.",
props: {
cards: { type: "array", description: "Array of card data objects" },
autoPlay: { type: "boolean", description: "Enable automatic flipping", default: "true" },
interval: { type: "number", description: "Interval between flips (ms)", default: "3000" },
}
}
const DEFAULT_CARDS: CardData[] = [
{
id: "card-1",
title: "Platinum Pulse",
number: "•••• •••• •••• 8842",
gradient: "linear-gradient(135deg, #FF3D77 0%, #FF8BA0 100%)",
secondaryColor: "#FFBDD0",
textColor: "#FFFFFF",
},
{
id: "card-2",
title: "Neon Nexus",
number: "•••• •••• •••• 4421",
gradient: "linear-gradient(135deg, #6E45FA 0%, #9B7EFA 100%)",
secondaryColor: "#C9BAFF",
textColor: "#FFFFFF",
},
{
id: "card-3",
title: "Cyber Slate",
number: "•••• •••• •••• 0098",
gradient: "linear-gradient(135deg, #222222 0%, #444444 100%)",
secondaryColor: "#888888",
textColor: "#FFFFFF",
},
]
const CardChip = () => (
<div className="relative w-12 h-9 rounded-md overflow-hidden bg-gradient-to-br from-yellow-200 via-yellow-400 to-yellow-600 p-[1px] shadow-[0_2px_4px_rgba(0,0,0,0.3)]">
<div className="w-full h-full bg-black/5 rounded-[5px] grid grid-cols-3 grid-rows-2 gap-[1px] p-[2px]">
{[...Array(6)].map((_, i) => (
<div key={i} className="bg-gradient-to-br from-yellow-50/70 to-transparent rounded-[1px]" />
))}
</div>
<div className="absolute top-1/2 left-0 w-full h-[0.5px] bg-black/30" />
<div className="absolute top-0 left-1/2 w-[0.5px] h-full bg-black/30" />
<div className="absolute inset-0 bg-gradient-to-tr from-transparent via-white/20 to-transparent" />
</div>
)
export default function ThreeDSmartCards({
cards = DEFAULT_CARDS,
className,
autoPlay = true,
interval = 3500
}: ThreeDSmartCardsProps) {
const [index, setIndex] = useState(0)
const containerRef = useRef<HTMLDivElement>(null)
// Mouse tracking for 3D tilt
const mouseX = useMotionValue(0)
const mouseY = useMotionValue(0)
const rotateX = useSpring(useTransform(mouseY, [-0.5, 0.5], [15, -15]), { stiffness: 150, damping: 20 })
const rotateY = useSpring(useTransform(mouseX, [-0.5, 0.5], [-20, 20]), { stiffness: 150, damping: 20 })
const handleMouseMove = (e: React.MouseEvent) => {
if (!containerRef.current) return
const rect = containerRef.current.getBoundingClientRect()
const x = (e.clientX - rect.left) / rect.width - 0.5
const y = (e.clientY - rect.top) / rect.height - 0.5
mouseX.set(x)
mouseY.set(y)
}
const handleMouseLeave = () => {
mouseX.set(0)
mouseY.set(0)
}
useEffect(() => {
if (!autoPlay) return
const timer = setInterval(() => {
setIndex((prev) => (prev + 1) % cards.length)
}, interval)
return () => clearInterval(timer)
}, [autoPlay, interval, cards.length])
return (
<div
ref={containerRef}
className={cn("relative w-[600px] h-[450px] flex items-center justify-center", className)}
style={{ perspective: "2000px" }}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
>
<motion.div
style={{
rotateX,
rotateY,
transformStyle: "preserve-3d",
}}
className="relative w-[360px] h-[225px]"
>
<AnimatePresence mode="popLayout">
{cards.map((card, i) => {
const relativeIndex = (i - index + cards.length) % cards.length
const isTop = relativeIndex === 0
return (
<motion.div
key={card.id}
initial={{
scale: 0.8,
opacity: 0,
z: -relativeIndex * 80,
x: 60,
y: 40
}}
animate={{
scale: 1 - relativeIndex * 0.05,
opacity: 1,
z: -relativeIndex * 80,
y: relativeIndex * 10,
x: isTop ? -50 : relativeIndex * 40,
rotateY: isTop ? -30 : -12,
rotateZ: relativeIndex * -1,
}}
exit={{
x: 300,
opacity: 0,
scale: 0.7,
z: 200,
rotateY: 60,
transition: { duration: 0.8, ease: [0.23, 1, 0.32, 1] }
}}
transition={{
type: "spring",
stiffness: 90,
damping: 15,
mass: 1.2
}}
style={{
position: "absolute",
inset: 0,
background: card.gradient,
borderRadius: 32,
padding: 32,
paddingBottom: 64, // Massive bottom padding to prevent clipping
color: card.textColor,
overflow: "hidden",
boxShadow: `
${isTop ? '0 50px 100px -20px rgba(0,0,0,0.6)' : '0 15px 40px -10px rgba(0,0,0,0.4)'},
inset 0 0 0 1px ${card.secondaryColor}55,
inset 0 2px 0 0 rgba(255,255,255,0.3),
inset 0 -2px 0 0 rgba(0,0,0,0.3)
`,
transformStyle: "preserve-3d",
zIndex: cards.length - relativeIndex,
backfaceVisibility: "hidden",
}}
className="cursor-pointer group"
onClick={() => setIndex((i + 1) % cards.length)}
>
{/* Brushed Metal Texture Overlay */}
<div className="absolute inset-0 opacity-[0.03] pointer-events-none bg-[url('https://grainy-gradients.vercel.app/noise.svg')] brightness-150 mix-blend-overlay" />
{/* Metallic Chrome Shine - Top Card Only */}
{isTop && (
<motion.div
className="absolute inset-0 rounded-[32px] overflow-hidden opacity-0 group-hover:opacity-100 transition-opacity duration-700 pointer-events-none"
style={{
background: "linear-gradient(110deg, transparent 15%, rgba(255,255,255,0.05) 20%, rgba(255,255,255,0.4) 35%, rgba(255,255,255,0.4) 45%, rgba(255,255,255,0.05) 60%, transparent 65%)",
backgroundSize: "250% 100%",
mixBlendMode: "overlay"
}}
animate={{
backgroundPosition: ["200% 0%", "-200% 0%"],
}}
transition={{
duration: 4,
repeat: Infinity,
ease: "linear"
}}
/>
)}
{/* Chiseled Rim Light */}
<div
className="absolute inset-0 pointer-events-none rounded-[32px] border border-white/20"
style={{
background: isTop
? "linear-gradient(135deg, rgba(255,255,255,0.2) 0%, transparent 30%, rgba(0,0,0,0.2) 100%)"
: "linear-gradient(to bottom, rgba(255,255,255,0.1), rgba(0,0,0,0.3))",
}}
/>
{/* 3D Translated Content - Fully visible on background cards but slightly darker */}
<div
className={cn(
"w-full h-full flex flex-col justify-between relative z-10 select-none transition-all duration-500",
!isTop && "brightness-[0.6] saturate-[0.8]"
)}
style={{ transform: `translateZ(${isTop ? 60 : 30}px)` }}
>
<div className="flex justify-between items-start">
<div className="flex flex-col gap-1 drop-shadow-2xl">
<div className="flex items-center gap-2">
<div className="w-1.5 h-1.5 rounded-full bg-white shadow-[0_0_8px_#fff]" />
<span className="text-[11px] font-black uppercase tracking-[0.3em] text-white/80">Master Vault</span>
</div>
<h3 className="text-3xl font-black tracking-tighter italic text-white drop-shadow-[0_4px_4px_rgba(0,0,0,0.5)]">{card.title}</h3>
</div>
<CardChip />
</div>
{/* Bottom Details - Heavily pushed up to prevent edge clipping */}
<div className="mt-auto flex flex-col gap-6 -translate-y-4">
<p className="text-2xl font-mono tracking-[0.2em] text-white/90 drop-shadow-[0_2px_10px_rgba(0,0,0,0.8)] font-bold">{card.number}</p>
<div className="flex justify-between items-end">
<div className="flex gap-8">
<div className="flex flex-col">
<span className="text-[9px] font-black uppercase tracking-widest text-white/60">Member since</span>
<span className="text-base font-black tabular-nums text-white">2026</span>
</div>
<div className="flex flex-col">
<span className="text-[9px] font-black uppercase tracking-widest text-white/60">Expiry date</span>
<span className="text-base font-black tabular-nums text-white">12/28</span>
</div>
</div>
<div className="relative w-14 h-14 flex items-center justify-center">
<div className="absolute inset-0 rounded-full bg-white/5 backdrop-blur-2xl border border-white/30 shadow-inner" />
<div className="w-9 h-9 rounded-full border-[4px] border-white/60 relative overflow-hidden shadow-2xl">
<div className="absolute top-0 -left-full w-full h-full bg-white/30 skew-x-[45deg] animate-pulse" />
</div>
</div>
</div>
</div>
</div>
</motion.div>
)
})}
</AnimatePresence>
</motion.div>
</div>
)
}"use client"
import { motion, useMotionValue, useSpring, useTransform, AnimatePresence } from "framer-motion"
import { useState, useEffect, useRef } from "react"
import { cn } from "@/lib/utils"
interface CardData {
id: string
title: string
number: string
gradient: string
textColor: string
secondaryColor: string
}
interface ThreeDSmartCardsProps {
cards?: CardData[]
className?: string
autoPlay?: boolean
interval?: number
}
export const componentMeta = {
name: "3D Smartcards",
description: "A premium 3D stacking card animation with holographic effects and smooth flipping transitions.",
props: {
cards: { type: "array", description: "Array of card data objects" },
autoPlay: { type: "boolean", description: "Enable automatic flipping", default: "true" },
interval: { type: "number", description: "Interval between flips (ms)", default: "3000" },
}
}
const DEFAULT_CARDS: CardData[] = [
{
id: "card-1",
title: "Platinum Pulse",
number: "•••• •••• •••• 8842",
gradient: "linear-gradient(135deg, #FF3D77 0%, #FF8BA0 100%)",
secondaryColor: "#FFBDD0",
textColor: "#FFFFFF",
},
{
id: "card-2",
title: "Neon Nexus",
number: "•••• •••• •••• 4421",
gradient: "linear-gradient(135deg, #6E45FA 0%, #9B7EFA 100%)",
secondaryColor: "#C9BAFF",
textColor: "#FFFFFF",
},
{
id: "card-3",
title: "Cyber Slate",
number: "•••• •••• •••• 0098",
gradient: "linear-gradient(135deg, #222222 0%, #444444 100%)",
secondaryColor: "#888888",
textColor: "#FFFFFF",
},
]
const CardChip = () => (
<div className="relative w-12 h-9 rounded-md overflow-hidden bg-gradient-to-br from-yellow-200 via-yellow-400 to-yellow-600 p-[1px] shadow-[0_2px_4px_rgba(0,0,0,0.3)]">
<div className="w-full h-full bg-black/5 rounded-[5px] grid grid-cols-3 grid-rows-2 gap-[1px] p-[2px]">
{[...Array(6)].map((_, i) => (
<div key={i} className="bg-gradient-to-br from-yellow-50/70 to-transparent rounded-[1px]" />
))}
</div>
<div className="absolute top-1/2 left-0 w-full h-[0.5px] bg-black/30" />
<div className="absolute top-0 left-1/2 w-[0.5px] h-full bg-black/30" />
<div className="absolute inset-0 bg-gradient-to-tr from-transparent via-white/20 to-transparent" />
</div>
)
export default function ThreeDSmartCards({
cards = DEFAULT_CARDS,
className,
autoPlay = true,
interval = 3500
}: ThreeDSmartCardsProps) {
const [index, setIndex] = useState(0)
const containerRef = useRef<HTMLDivElement>(null)
// Mouse tracking for 3D tilt
const mouseX = useMotionValue(0)
const mouseY = useMotionValue(0)
const rotateX = useSpring(useTransform(mouseY, [-0.5, 0.5], [15, -15]), { stiffness: 150, damping: 20 })
const rotateY = useSpring(useTransform(mouseX, [-0.5, 0.5], [-20, 20]), { stiffness: 150, damping: 20 })
const handleMouseMove = (e: React.MouseEvent) => {
if (!containerRef.current) return
const rect = containerRef.current.getBoundingClientRect()
const x = (e.clientX - rect.left) / rect.width - 0.5
const y = (e.clientY - rect.top) / rect.height - 0.5
mouseX.set(x)
mouseY.set(y)
}
const handleMouseLeave = () => {
mouseX.set(0)
mouseY.set(0)
}
useEffect(() => {
if (!autoPlay) return
const timer = setInterval(() => {
setIndex((prev) => (prev + 1) % cards.length)
}, interval)
return () => clearInterval(timer)
}, [autoPlay, interval, cards.length])
return (
<div
ref={containerRef}
className={cn("relative w-[600px] h-[450px] flex items-center justify-center", className)}
style={{ perspective: "2000px" }}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
>
<motion.div
style={{
rotateX,
rotateY,
transformStyle: "preserve-3d",
}}
className="relative w-[360px] h-[225px]"
>
<AnimatePresence mode="popLayout">
{cards.map((card, i) => {
const relativeIndex = (i - index + cards.length) % cards.length
const isTop = relativeIndex === 0
return (
<motion.div
key={card.id}
initial={{
scale: 0.8,
opacity: 0,
z: -relativeIndex * 80,
x: 60,
y: 40
}}
animate={{
scale: 1 - relativeIndex * 0.05,
opacity: 1,
z: -relativeIndex * 80,
y: relativeIndex * 10,
x: isTop ? -50 : relativeIndex * 40,
rotateY: isTop ? -30 : -12,
rotateZ: relativeIndex * -1,
}}
exit={{
x: 300,
opacity: 0,
scale: 0.7,
z: 200,
rotateY: 60,
transition: { duration: 0.8, ease: [0.23, 1, 0.32, 1] }
}}
transition={{
type: "spring",
stiffness: 90,
damping: 15,
mass: 1.2
}}
style={{
position: "absolute",
inset: 0,
background: card.gradient,
borderRadius: 32,
padding: 32,
paddingBottom: 64, // Massive bottom padding to prevent clipping
color: card.textColor,
overflow: "hidden",
boxShadow: `
${isTop ? '0 50px 100px -20px rgba(0,0,0,0.6)' : '0 15px 40px -10px rgba(0,0,0,0.4)'},
inset 0 0 0 1px ${card.secondaryColor}55,
inset 0 2px 0 0 rgba(255,255,255,0.3),
inset 0 -2px 0 0 rgba(0,0,0,0.3)
`,
transformStyle: "preserve-3d",
zIndex: cards.length - relativeIndex,
backfaceVisibility: "hidden",
}}
className="cursor-pointer group"
onClick={() => setIndex((i + 1) % cards.length)}
>
{/* Brushed Metal Texture Overlay */}
<div className="absolute inset-0 opacity-[0.03] pointer-events-none bg-[url('https://grainy-gradients.vercel.app/noise.svg')] brightness-150 mix-blend-overlay" />
{/* Metallic Chrome Shine - Top Card Only */}
{isTop && (
<motion.div
className="absolute inset-0 rounded-[32px] overflow-hidden opacity-0 group-hover:opacity-100 transition-opacity duration-700 pointer-events-none"
style={{
background: "linear-gradient(110deg, transparent 15%, rgba(255,255,255,0.05) 20%, rgba(255,255,255,0.4) 35%, rgba(255,255,255,0.4) 45%, rgba(255,255,255,0.05) 60%, transparent 65%)",
backgroundSize: "250% 100%",
mixBlendMode: "overlay"
}}
animate={{
backgroundPosition: ["200% 0%", "-200% 0%"],
}}
transition={{
duration: 4,
repeat: Infinity,
ease: "linear"
}}
/>
)}
{/* Chiseled Rim Light */}
<div
className="absolute inset-0 pointer-events-none rounded-[32px] border border-white/20"
style={{
background: isTop
? "linear-gradient(135deg, rgba(255,255,255,0.2) 0%, transparent 30%, rgba(0,0,0,0.2) 100%)"
: "linear-gradient(to bottom, rgba(255,255,255,0.1), rgba(0,0,0,0.3))",
}}
/>
{/* 3D Translated Content - Fully visible on background cards but slightly darker */}
<div
className={cn(
"w-full h-full flex flex-col justify-between relative z-10 select-none transition-all duration-500",
!isTop && "brightness-[0.6] saturate-[0.8]"
)}
style={{ transform: `translateZ(${isTop ? 60 : 30}px)` }}
>
<div className="flex justify-between items-start">
<div className="flex flex-col gap-1 drop-shadow-2xl">
<div className="flex items-center gap-2">
<div className="w-1.5 h-1.5 rounded-full bg-white shadow-[0_0_8px_#fff]" />
<span className="text-[11px] font-black uppercase tracking-[0.3em] text-white/80">Master Vault</span>
</div>
<h3 className="text-3xl font-black tracking-tighter italic text-white drop-shadow-[0_4px_4px_rgba(0,0,0,0.5)]">{card.title}</h3>
</div>
<CardChip />
</div>
{/* Bottom Details - Heavily pushed up to prevent edge clipping */}
<div className="mt-auto flex flex-col gap-6 -translate-y-4">
<p className="text-2xl font-mono tracking-[0.2em] text-white/90 drop-shadow-[0_2px_10px_rgba(0,0,0,0.8)] font-bold">{card.number}</p>
<div className="flex justify-between items-end">
<div className="flex gap-8">
<div className="flex flex-col">
<span className="text-[9px] font-black uppercase tracking-widest text-white/60">Member since</span>
<span className="text-base font-black tabular-nums text-white">2026</span>
</div>
<div className="flex flex-col">
<span className="text-[9px] font-black uppercase tracking-widest text-white/60">Expiry date</span>
<span className="text-base font-black tabular-nums text-white">12/28</span>
</div>
</div>
<div className="relative w-14 h-14 flex items-center justify-center">
<div className="absolute inset-0 rounded-full bg-white/5 backdrop-blur-2xl border border-white/30 shadow-inner" />
<div className="w-9 h-9 rounded-full border-[4px] border-white/60 relative overflow-hidden shadow-2xl">
<div className="absolute top-0 -left-full w-full h-full bg-white/30 skew-x-[45deg] animate-pulse" />
</div>
</div>
</div>
</div>
</div>
</motion.div>
)
})}
</AnimatePresence>
</motion.div>
</div>
)
}Save to favoritesCopy to Figma
React
3D Smartcards
A premium 3D stacking card animation with holographic effects and smooth flipping transitions.
Installation
$ npx @cosmoo/oblivion add three-d-smartcardsDependencies
npm install framer-motionUsage Example
tsx
import ThreeDSmartcards from "@/components/oblivion/three-d-smartcards"
export default function Demo() {
return (
<div className="p-10 flex justify-center">
<ThreeDSmartcards />
</div>
)
}import ThreeDSmartcards from "@/components/oblivion/three-d-smartcards"
export default function Demo() {
return (
<div className="p-10 flex justify-center">
<ThreeDSmartcards />
</div>
)
}