Three-dimensional perspective card with layered Z-depth planes, moving specular highlights, and a pointer-driven shifting holographic foil gradient.
Preview
Holographic Foil Surface
Pointer coordinates drive both 3D perspective pitch/roll and spectral diffraction angle.
Customize
4 propsInstallation
Copies the component source and its CSS Module into your project.
npx motionfoundry add holo-tilt-cardWrites components/ui/holo-tilt-card.tsx and components/styles/holo-tilt-card.module.css.
Usage
Updates live as you change the controls — only props that differ from the defaults are included.
1import { HoloTiltCard } from "@motionfoundry/ui";2 3export function Example() {4 return (5 <HoloTiltCard />6 );7}Props
Accessibility
Smoothly restores the card to its flat resting plane on pointer leave and disables 3D perspective rotation when prefers-reduced-motion is active.
Toggle prefers-reduced-motion in your OS or browser devtools (Rendering → Emulate CSS media feature) to verify the reduced-motion state.
Source
1"use client";2 3import * as React from "react";4import { motion, useMotionValue, useSpring, useReducedMotion } from "motion/react";5import styles from "../styles/holo-tilt-card.module.css";6 7export interface HoloTiltCardProps {8 /** Card title */9 title?: string;10 /** Supporting subtitle */11 subtitle?: string;12 /** Maximum tilt angle in degrees */13 maxTilt?: number;14 /** Holographic foil opacity (0.05 to 0.5) */15 holoOpacity?: number;16 /** CSS perspective distance in pixels */17 perspective?: number;18 /** Optional children */19 children?: React.ReactNode;20 /** Optional className */21 className?: string;22}23 24export function HoloTiltCard({25 title = "Holographic Foil Surface",26 subtitle = "Pointer coordinates drive both 3D perspective pitch/roll and spectral diffraction angle.",27 maxTilt = 14,28 holoOpacity = 0.24,29 perspective = 920,30 children,31 className = "",32}: HoloTiltCardProps) {33 const ref = React.useRef<HTMLDivElement>(null);34 const prefersReducedMotion = useReducedMotion();35 36 const rawRx = useMotionValue(0);37 const rawRy = useMotionValue(0);38 const rotateX = useSpring(rawRx, { stiffness: 240, damping: 22 });39 const rotateY = useSpring(rawRy, { stiffness: 240, damping: 22 });40 const [holoState, setHoloState] = React.useState({ x: 50, y: 50, angle: 125, active: false });41 42 const handlePointerMove = (e: React.PointerEvent<HTMLDivElement>) => {43 if (prefersReducedMotion || !ref.current) return;44 const rect = ref.current.getBoundingClientRect();45 const px = (e.clientX - rect.left) / rect.width;46 const py = (e.clientY - rect.top) / rect.height;47 rawRx.set((0.5 - py) * maxTilt * 2);48 rawRy.set((px - 0.5) * maxTilt * 2);49 setHoloState({50 x: px * 100,51 y: py * 100,52 angle: 110 + (px - 0.5) * 90,53 active: true,54 });55 };56 57 const handlePointerLeave = () => {58 rawRx.set(0);59 rawRy.set(0);60 setHoloState((prev) => ({ ...prev, active: false }));61 };62 63 return (64 <div style={{ perspective: `${perspective}px` }} className={styles.stage}>65 <motion.div66 ref={ref}67 onPointerMove={handlePointerMove}68 onPointerLeave={handlePointerLeave}69 style={{70 rotateX: prefersReducedMotion ? 0 : rotateX,71 rotateY: prefersReducedMotion ? 0 : rotateY,72 transformStyle: "preserve-3d",73 }}74 className={`${styles.card} ${className}`.trim()}75 >76 <div77 aria-hidden="true"78 className={styles.holoLayer}79 style={{80 opacity: holoState.active && !prefersReducedMotion ? holoOpacity : 0.06,81 backgroundImage: `linear-gradient(${holoState.angle}deg, rgba(56,189,248,0.55) 0%, rgba(168,85,247,0.55) 35%, rgba(244,114,182,0.5) 68%, rgba(52,211,153,0.55) 100%)`,82 backgroundPosition: `${holoState.x}% ${holoState.y}%`,83 }}84 />85 <div86 aria-hidden="true"87 className={styles.specularGlare}88 style={{89 opacity: holoState.active && !prefersReducedMotion ? 0.18 : 0,90 background: `radial-gradient(circle at ${holoState.x}% ${holoState.y}%, rgba(255,255,255,0.9), transparent 60%)`,91 }}92 />93 <div className={styles.depthContent}>94 <span className={styles.badge}>3D Holographic Depth</span>95 <h3 className={styles.title}>{title}</h3>96 <p className={styles.subtitle}>{subtitle}</p>97 {children}98 </div>99 </motion.div>100 </div>101 );102}