Velocity UI
Loading…
Menu

Holographic Foil Card

A premium 3D tilt-reactive card with a shimmering holographic foil effect. Uses Framer Motion for smooth physics and CSS mix-blend-modes for realistic lighting.

Installation

Add this component to your project using the CLI:

terminal
npx vui-registry-cli-v1 add holographic-card

Source Code

holographic-card.tsx
'use client'

import React, { useRef, useState } from 'react'
import { motion, useMotionTemplate, useMotionValue, useSpring, useTransform } from 'framer-motion'
import { cn } from '@/lib/utils'

interface HolographicCardProps {
  className?: string
  backgroundImage?: string
}

export function HolographicCard({ 
  className, 
  backgroundImage = "https://images.unsplash.com/photo-1688398658141-d39eda099217?q=80&w=687&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
}: HolographicCardProps) {
  const containerRef = useRef<HTMLDivElement>(null)
  const [isHovered, setIsHovered] = useState(false)
  
  // Set initial mouse values to the center of a 400x600 coordinate space
  // This ensures the card starts perfectly straight
  const mouseX = useMotionValue(200)
  const mouseY = useMotionValue(300)

  const springConfig = { damping: 30, stiffness: 100, mass: 1 }
  const smoothX = useSpring(mouseX, springConfig)
  const smoothY = useSpring(mouseY, springConfig)

  // Tilt Logic (Straight at 200/300)
  const rotateX = useTransform(smoothY, [0, 600], [15, -15])
  const rotateY = useTransform(smoothX, [0, 400], [-15, 15])

  // Internal Parallax - Reduced range so the image doesn't crop
  const bgX = useTransform(smoothX, [0, 400], [10, -10])
  const bgY = useTransform(smoothY, [0, 600], [10, -10])
  
  const contentX = useTransform(smoothX, [0, 400], [-10, 10])
  const contentY = useTransform(smoothY, [0, 600], [-10, 10])

  const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
    if (!containerRef.current) return
    const rect = containerRef.current.getBoundingClientRect()
    mouseX.set(e.clientX - rect.left)
    mouseY.set(e.clientY - rect.top)
  }

  const handleMouseEnter = () => setIsHovered(true)
  
  const handleMouseLeave = () => {
    setIsHovered(false)
    mouseX.set(200) 
    mouseY.set(300)
  }

  return (
    <div className={cn("relative w-full h-[600px] flex items-center justify-center [perspective:1500px]", className)}>
      <motion.div
        ref={containerRef}
        onMouseMove={handleMouseMove}
        onMouseEnter={handleMouseEnter}
        onMouseLeave={handleMouseLeave}
        style={{
          rotateX,
          rotateY,
          transformStyle: "preserve-3d",
        }}
        // Smaller card width (max-w-[300px]) to prevent cropping and look more balanced
        className="group relative w-full max-w-[310px] aspect-[3/4.2] rounded-[2.2rem] bg-black border border-white/10 shadow-[0_20px_50px_rgba(0,0,0,0.5)] will-change-transform overflow-hidden [isolation:isolate]"
      >
        {/* --- LAYER 1: BACKGROUND IMAGE --- */}
        <motion.div
          className="absolute inset-[-5%] z-0 pointer-events-none"
          style={{
            x: bgX,
            y: bgY,
            backgroundImage: `url(${backgroundImage})`,
            backgroundSize: "cover",
            backgroundPosition: "center",
            // Initially dimmer, becomes vibrant on hover
            filter: isHovered ? "brightness(1) saturate(1.2)" : "brightness(0.5) saturate(0.8)",
            transition: "filter 0.5s ease-out"
          }}
        >
          <div className="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-black/20" />
        </motion.div>

        {/* --- LAYER 2: HOLOGRAPHIC SHIMMER --- */}
        <motion.div
          className="absolute inset-0 z-10 pointer-events-none mix-blend-color-dodge opacity-0 group-hover:opacity-100 transition-opacity duration-500"
          style={{
            background: useMotionTemplate`
              radial-gradient(
                600px circle at ${smoothX}px ${smoothY}px,
                rgba(255, 255, 255, 0.3) 0%,
                rgba(0, 240, 255, 0.1) 40%,
                transparent 80%
              )
            `,
          }}
        />

        {/* --- LAYER 3: CRYSTAL GLARE --- */}
        <motion.div
          className="absolute inset-0 z-20 pointer-events-none mix-blend-overlay opacity-0 group-hover:opacity-100 transition-opacity duration-700"
          style={{
            background: useMotionTemplate`
              linear-gradient(
                135deg,
                transparent 35%,
                rgba(255, 255, 255, 0.4) 50%,
                transparent 65%
              )
            `,
            backgroundSize: "200% 200%",
            backgroundPosition: useMotionTemplate`${useTransform(smoothX, [0, 400], [-100, 100])}% center`,
          }}
        />

        {/* --- LAYER 4: CONTENT --- */}
        <motion.div
          className="relative z-50 w-full h-full p-8 flex flex-col justify-between pointer-events-none"
          style={{ 
            x: contentX, 
            y: contentY, 
            translateZ: "60px",
            transformStyle: "preserve-3d" 
          }}
        >
          <div className="space-y-3">
            <motion.div 
              animate={{ opacity: isHovered ? 1 : 0.6 }}
              className="inline-block px-3 py-1 rounded-full bg-black/40 backdrop-blur-xl border border-white/10"
            >
              <span className="text-[9px] text-white font-bold uppercase tracking-[0.2em]">
                Limited Artifact
              </span>
            </motion.div>
            <h3 className="text-4xl font-black text-white italic tracking-tighter leading-[0.9] drop-shadow-[0_10px_15px_rgba(0,0,0,0.8)]">
              DARK <br /> NEBULA
            </h3>
          </div>

          <div className="flex justify-between items-end">
            <div className="flex flex-col gap-0.5">
              <span className="text-[9px] uppercase tracking-widest text-zinc-400 font-bold">Registry Status</span>
              <span className="text-xl font-black text-white">
                LEGENDARY
              </span>
            </div>
            <div className="w-12 h-12 rounded-xl border border-white/20 bg-black/40 backdrop-blur-xl flex items-center justify-center text-2xl font-black text-white">
              Ω
            </div>
          </div>
        </motion.div>

        {/* Final Outer Rim */}
        <div className="absolute inset-0 z-[60] rounded-[2.2rem] border-2 border-white/5 pointer-events-none group-hover:border-white/20 transition-colors duration-500" />
      </motion.div>
    </div>
  )
}

Dependencies

  • framer-motion: latest
  • clsx: latest
  • tailwind-merge: latest

Props

Component property reference.

NameTypeDefaultDescription
childrenReactNode-Content to display inside the card.
classNamestring-Additional CSS classes.
Context Worth Keeping In Orbit

Most components here are inspired by outstanding libraries and creators in the ecosystem. I don’t claim to be the original author — this is my space for learning, rebuilding, and understanding great work at a deeper level.

I’m still a student of the craft, constantly studying the best and translating what I learn through my own perspective. Every piece reflects curiosity, respect for the community, and small creative touches that feel true to me.

I’ve done my best to credit inspirations properly. If anything is missing or inaccurate, I truly appreciate a message so it can be corrected with care.