shadimage
gaia
uiholo-cardsource: ok · refreshed

Holo Card

An interactive 3D holographic profile card with tilt effects, sparkle animations, and flip functionality.

npx shadcn add "https://ui.heygaia.io/r/holo-card.json"
free

Where it goes

profile

Visual style

3dparticleplayful

Interaction

hover-3d

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
react-parallax-tilt

Files2 · source cached 10/7/2026, 7:11:34 PM

registry/new-york/ui/holo-card.tsx
"use client";

import Image from "next/image";
import type React from "react";
import { useRef, useState, useCallback } from "react";
import Tilt from "react-parallax-tilt";
import { cn } from "@/lib/utils";
import "./holo-card.css";

// ============================================================================
// Types
// ============================================================================

export interface HoloCardDisplayData {
	/** Primary name displayed on the card */
	name: string;
	/** Secondary text (e.g., personality phrase, tagline) */
	subtitle?: string;
	/** Extended description shown on the back of the card */
	description?: string;
	/** Primary identifier (e.g., user number, ID) */
	primaryId?: string | number;
	/** Secondary info (e.g., member since date) */
	secondaryInfo?: string;
	/** Background image URL */
	backgroundImage?: string;
	/** Badge/tag text (e.g., house name, role) */
	badge?: string;
	/** Overlay color for the card */
	overlayColor?: string;
	/** Overlay opacity (0-100) */
	overlayOpacity?: number;
}

export interface HoloCardBranding {
	/** Primary logo image URL */
	logo?: string;
	/** Logo alt text */
	logoAlt?: string;
	/** Icon shown in various places */
	icon?: string;
	/** Icon alt text */
	iconAlt?: string;
}

export interface HoloCardProps {
	/** Card display data */
	data: HoloCardDisplayData;
	/** Card dimensions */
	height?: number;
	width?: number;
	/** Show sparkle animation effect */
	showSparkles?: boolean;
	/** Force card to show front or back (disables flip) */
	forceSide?: "front" | "back";
	/** Custom branding (logos, icons) */
	branding?: HoloCardBranding;
	/** Additional CSS classes */
	className?: string;
	/** Children rendered inside the card */
	children?: React.ReactNode;
	/** Callback when card is flipped */
	onFlip?: (isFlipped: boolean) => void;
}

// ============================================================================
// Default Values
// ============================================================================

const DEFAULT_BACKGROUND =
	"https://i.pinimg.com/1200x/27/0a/74/270a74bdc412f9eeae4d2403ebc9bd63.jpg";

const DEFAULT_BRANDING: HoloCardBranding = {
	logo: "/images/logos/text_w_logo_white.webp",
	logoAlt: "Logo",
	icon: "/images/logos/experience_logo.svg",
	iconAlt: "Icon",
};

// ============================================================================
// Component
// ============================================================================

export const HoloCard = ({
	data,
	height = 446,
	width = 320,
	showSparkles = true,
	forceSide,
	branding = DEFAULT_BRANDING,
	className,
	children,
	onFlip,
}: HoloCardProps) => {
	const [hover, setHover] = useState(false);
	const [animated, setAnimated] = useState(true);
	const [isFlipped, setIsFlipped] = useState(false);
	const [activeBackgroundPosition, setActiveBackgroundPosition] = useState({
		tp: 0,
		lp: 0,
	});
	const ref = useRef<HTMLDivElement>(null);

	const {
		name,
		subtitle,
		description,
		primaryId,
		secondaryInfo,
		backgroundImage = DEFAULT_BACKGROUND,
		badge,
		overlayColor,
		overlayOpacity = 40,
	} = data;

	const handleCardClick = useCallback(() => {
		if (!forceSide) {
			const newFlippedState = !isFlipped;
			setIsFlipped(newFlippedState);
			onFlip?.(newFlippedState);
		}
	}, [forceSide, isFlipped, onFlip]);

	const handleOnMouseMove = useCallback(
		(event: React.MouseEvent<HTMLDivElement>) => {
			setAnimated(false);
			setHover(true);

			const card = ref.current;
			const l = event.nativeEvent.offsetX;
			const t = event.nativeEvent.offsetY;

			const h = card ? card.clientHeight : 0;
			const w = card ? card.clientWidth : 0;

			const px = Math.abs(Math.floor((100 / w) * l) - 100);
			const py = Math.abs(Math.floor((100 / h) * t) - 100);

			const lp = 50 + (px - 50) / 1.5;
			const tp = 50 + (py - 50) / 1.5;

			setActiveBackgroundPosition({ lp, tp });
		},
		[],
	);

	const handleOnTouchMove = useCallback(
		(event: React.TouchEvent<HTMLDivElement>) => {
			setAnimated(false);
			setHover(true);

			const card = ref.current;
			if (!card) return;

			const touch = event.touches[0];
			const rect = card.getBoundingClientRect();
			const l = touch.clientX - rect.left;
			const t = touch.clientY - rect.top;

			const h = card.clientHeight;
			const w = card.clientWidth;

			const px = Math.abs(Math.floor((100 / w) * l) - 100);
			const py = Math.abs(Math.floor((100 / h) * t) - 100);

			const lp = 50 + (px - 50) / 1.5;
			const tp = 50 + (py - 50) / 1.5;

			setActiveBackgroundPosition({ lp, tp });
		},
		[],
	);

	const handleOnMouseOut = useCallback(() => {
		setHover(false);
		setAnimated(true);
	}, []);

	const effectiveFlipped = forceSide ? forceSide === "back" : isFlipped;

	// Static mode styles for download/static rendering
	const containerStyle = forceSide
		? {
				perspective: "none",
				transform: "none",
			}
		: {
				perspective: "1000px",
				cursor: "pointer",
			};

	const innerStyle = forceSide
		? {
				transform: "none",
				position: "relative" as const,
				height: `${height}px`,
				width: `${width}px`,
			}
		: {
				transformStyle: "preserve-3d" as const,
				transform: effectiveFlipped ? "rotateY(180deg)" : "rotateY(0deg)",
				height: `${height}px`,
				width: `${width}px`,
			};

	const frontStyle = forceSide
		? {
				display: forceSide === "front" ? "block" : "none",
				position: "absolute" as const,
				inset: 0,
			}
		: {
				position: "absolute" as const,
				inset: 0,
				backfaceVisibility: "hidden" as const,
				WebkitBackfaceVisibility: "hidden" as const,
			};

	const backStyle = forceSide
		? {
				display: forceSide === "back" ? "block" : "none",
				position: "absolute" as const,
				inset: 0,
				transform: "none",
			}
		: {
				position: "absolute" as const,
				inset: 0,
				backfaceVisibility: "hidden" as const,
				WebkitBackfaceVisibility: "hidden" as const,
				transform: "rotateY(180deg)",
			};

	const holoCardClasses = cn(
		"holo-card",
		hover && "holo-card--active",
		animated && "holo-card--animated",
		showSparkles && "holo-card--sparkles",
	);

	const holoCardStyle = {
		"--holo-width": `${width}px`,
		"--holo-height": `${height}px`,
		backgroundImage: `url(${backgroundImage})`,
		backgroundPosition: hover
			? `${activeBackgroundPosition.lp}% ${activeBackgroundPosition.tp}%`
			: "center",
	} as React.CSSProperties;

	// Overlay component
	const OverlayLayer = overlayColor ? (
		<div
			className="pointer-events-none absolute inset-0 z-[3]"
			style={{
				background: overlayColor,
				mixBlendMode: "overlay",
				opacity: overlayOpacity / 100,
			}}
		/>
	) : null;

	// Front card content
	const FrontContent = (
		<div className="pointer-events-none absolute z-[2] flex h-full w-full flex-col items-start justify-end p-3 text-white transition">
			{/* Top bar with logo and badge */}
			<div className="absolute top-4 left-0 flex w-full justify-between px-3">
				{branding.logo && (
					<div className="rounded-full bg-white/30 p-1 px-2 font-serif text-xl font-light text-white/70 backdrop-blur-md">
						<Image
							src={branding.logo}
							alt={branding.logoAlt || "Logo"}
							width={100}
							height={30}
							className="object-contain"
						/>
					</div>
				)}
				{badge && (
					<div className="rounded-full bg-white/20 p-1 px-4 font-serif text-xl font-light text-white/70 backdrop-blur-md">
						{badge}
					</div>
				)}
			</div>

			{/* Background icon */}
			{branding.icon && (
				<Image
					src={branding.icon}
					alt={branding.iconAlt || "Icon"}
					className="scale-125 opacity-10"
					fill
				/>
			)}

			{/* Bottom info card */}
			<div className="relative flex w-full flex-col gap-1 overflow-hidden rounded-2xl bg-black/20 p-3 backdrop-blur-md">
				<div className="font-serif text-4xl font-bold text-white">{name}</div>
				{subtitle && (
					<div className="mb-10 font-light text-white italic">{subtitle}</div>
				)}

				<div className="flex w-full items-center justify-between">
					<div className="flex flex-col items-start gap-1">
						{primaryId && (
							<span className="text-sm text-white/80">User {primaryId}</span>
						)}
						{secondaryInfo && (
							<span className="text-xs text-white/50">{secondaryInfo}</span>
						)}
					</div>

					{branding.icon && (
						<div className="flex gap-2">
							<Image
								src={branding.icon}
								alt={branding.iconAlt || "Icon"}
								width={30}
								height={30}
							/>
						</div>
					)}
				</div>
			</div>
		</div>
	);

	// Back card content
	const BackContent = (
		<div className="pointer-events-none absolute z-[2] flex h-full w-full flex-col items-start justify-between p-3 text-white">
			<div className="flex w-full flex-col gap-4">
				{/* Top bar */}
				<div className="flex items-center justify-between">
					{branding.logo && (
						<div className="rounded-full bg-white/30 p-1 px-2 backdrop-blur-md">
							<Image
								src={branding.logo}
								alt={branding.logoAlt || "Logo"}
								width={80}
								height={24}
								className="object-contain"
							/>
						</div>
					)}
					{badge && (
						<div className="rounded-full bg-white/20 p-1 px-3 font-serif text-xl font-light text-white/70 backdrop-blur-md">
							{badge}
						</div>
					)}
				</div>

				{/* Bio section */}
				<div className="relative overflow-hidden rounded-2xl bg-black/20 p-4 backdrop-blur-md">
					<div className="mb-2 font-serif text-2xl font-bold text-white">
						{name}
					</div>
					{subtitle && (
						<div className="mb-4 text-sm font-light text-white/80 italic">
							{subtitle}
						</div>
					)}
					{description && (
						<p className="text-sm text-white/80">{description}</p>
					)}
				</div>
			</div>

			{/* Bottom info */}
			<div className="flex w-full items-center justify-between rounded-xl bg-black/20 p-3 backdrop-blur-md">
				<div className="flex flex-col gap-1">
					<span className="text-xs text-white/50">Member Since</span>
					<span className="text-sm font-medium text-white/80">
						{secondaryInfo || "—"}
					</span>
				</div>
				<div className="flex flex-col items-end gap-1">
					<span className="text-xs text-white/50">User ID</span>
					<span className="text-sm font-medium text-white/80">
						{primaryId || "—"}
					</span>
				</div>
			</div>
		</div>
	);

	// Render card face (either static or with tilt)
	const renderCardFace = (
		content: React.ReactNode,
		isStatic: boolean = false,
	) => {
		if (isStatic || forceSide) {
			return (
				<div className="relative h-full w-full overflow-hidden rounded-2xl shadow-xl">
					{OverlayLayer}
					{content}
					<div ref={ref} className={holoCardClasses} style={holoCardStyle}>
						{children}
					</div>
				</div>
			);
		}

		return (
			<Tilt className="relative h-full w-full overflow-hidden rounded-2xl p-0! shadow-xl">
				{OverlayLayer}
				{content}
				<div
					ref={ref}
					className={holoCardClasses}
					style={holoCardStyle}
					onMouseMove={handleOnMouseMove}
					onTouchMove={handleOnTouchMove}
					onMouseOut={handleOnMouseOut}
					onBlur={handleOnMouseOut}
				>
					{children}
				</div>
			</Tilt>
		);
	};

	const handleKeyDown = useCallback(
		(event: React.KeyboardEvent) => {
			if (event.key === "Enter" || event.key === " ") {
				event.preventDefault();
				handleCardClick();
			}
		},
		[handleCardClick],
	);

	return (
		<div
			className={cn(forceSide ? "" : "perspective-1000", className)}
			onClick={handleCardClick}
			onKeyDown={!forceSide ? handleKeyDown : undefined}
			role={!forceSide ? "button" : undefined}
			tabIndex={!forceSide ? 0 : undefined}
			title={
				!forceSide ? `Profile card for ${name}. Click to flip.` : undefined
			}
			style={containerStyle}
		>
			<div
				className={
					forceSide ? "relative" : "relative transition-transform duration-700"
				}
				style={innerStyle}
			>
				{/* Front Side */}
				<div style={frontStyle}>
					{renderCardFace(FrontContent, !!forceSide)}
				</div>

				{/* Back Side */}
				<div style={backStyle}>{renderCardFace(BackContent, !!forceSide)}</div>
			</div>
		</div>
	);
};

export default HoloCard;
registry/new-york/ui/holo-card.css
/* HoloCard Animations and Styles */

@keyframes holoSparkle {
	0%,
	100% {
		opacity: 0.75;
		filter: brightness(1.2) contrast(1.25);
	}
	5%,
	8% {
		opacity: 1;
		filter: brightness(0.8) contrast(1.2);
	}
	13%,
	16% {
		opacity: 0.5;
		filter: brightness(1.2) contrast(0.8);
	}
	35%,
	38% {
		opacity: 1;
		filter: brightness(1) contrast(1);
	}
	55% {
		opacity: 0.33;
		filter: brightness(1.2) contrast(1.25);
	}
}

@keyframes holoGradient {
	0%,
	100% {
		opacity: 0.5;
		background-position: 50% 50%;
		filter: brightness(0.5) contrast(1);
	}
	5%,
	9% {
		background-position: 100% 100%;
		opacity: 1;
		filter: brightness(0.75) contrast(1.25);
	}
	13%,
	17% {
		background-position: 0% 0%;
		opacity: 0.88;
	}
	35%,
	39% {
		background-position: 100% 100%;
		opacity: 1;
		filter: brightness(0.5) contrast(1);
	}
	55% {
		background-position: 0% 0%;
		opacity: 1;
		filter: brightness(0.75) contrast(1.25);
	}
}

.holo-card {
	--holo-width: 320px;
	--holo-height: 446px;

	width: var(--holo-width);
	height: var(--holo-height);
	background-color: #211799;
	background-size: cover;
	background-repeat: no-repeat;
	background-position: center;
	border-radius: 5% / 3.5%;
	box-shadow:
		-3px -3px 3px 0 rgba(38, 230, 247, 0.3),
		3px 3px 3px 0 rgba(247, 89, 228, 0.3),
		0 0 6px 2px rgba(255, 231, 89, 0.3),
		0 35px 25px -15px rgba(0, 0, 0, 0.3);
	position: relative;
	overflow: hidden;
	display: inline-block;
	vertical-align: middle;
	transition: transform 0.3s ease;
}

/* Pseudo-elements for holographic effects */
.holo-card::before,
.holo-card::after {
	content: "";
	position: absolute;
	left: 0;
	right: 0;
	top: 0;
	bottom: 0;
	background-position: 0% 0%;
	background-repeat: no-repeat;
	background-size: 300% 300%;
	mix-blend-mode: color-dodge;
	opacity: 0.2;
	z-index: 1;
	background-image: linear-gradient(
		115deg,
		transparent 0%,
		#54a29e 25%,
		transparent 47%,
		transparent 53%,
		#a79d66 75%,
		transparent 100%
	);
}

/* Sparkles effect */
.holo-card--sparkles::after {
	background-image:
		url("https://assets.codepen.io/13471/sparkles.gif"),
		linear-gradient(
			125deg,
			rgba(255, 0, 132, 0.31) 15%,
			rgba(252, 164, 0, 0.25) 30%,
			rgba(255, 255, 0, 0.19) 40%,
			rgba(0, 255, 138, 0.13) 60%,
			rgba(0, 207, 255, 0.25) 70%,
			rgba(204, 76, 250, 0.31) 85%
		);
	background-position: center;
	background-size: 180%;
	mix-blend-mode: color-dodge;
	opacity: 1;
	z-index: 1;
}

/* Active/hover state */
.holo-card--active::before {
	opacity: 1;
	animation: none;
	transition: none;
	background-image: linear-gradient(
		110deg,
		transparent 25%,
		#54a29e 48%,
		#a79d66 52%,
		transparent 75%
	);
}

/* Animated state (default) */
.holo-card--animated {
	transition: transform 1s ease;
}

.holo-card--animated::before {
	transition: all 1s ease;
	animation: holoGradient 12s ease infinite;
}

.holo-card--animated::after {
	transition: all 1s ease;
	animation: holoSparkle 12s ease infinite;
}

/* Card flip container */
.holo-card-container {
	perspective: 1000px;
	cursor: pointer;
}

.holo-card-container--static {
	perspective: none;
	cursor: default;
}

.holo-card-inner {
	position: relative;
	transition: transform 0.7s ease;
	transform-style: preserve-3d;
}

.holo-card-inner--flipped {
	transform: rotateY(180deg);
}

.holo-card-face {
	position: absolute;
	inset: 0;
	backface-visibility: hidden;
	-webkit-backface-visibility: hidden;
}

.holo-card-face--back {
	transform: rotateY(180deg);
}

/* Light mode adjustments */
:root[data-theme="light"] .holo-card,
.light .holo-card {
	box-shadow:
		-3px -3px 3px 0 rgba(38, 230, 247, 0.2),
		3px 3px 3px 0 rgba(247, 89, 228, 0.2),
		0 0 6px 2px rgba(255, 231, 89, 0.2),
		0 35px 25px -15px rgba(0, 0, 0, 0.15);
}

More from gaia

Browse all components in this library →