Holo Card

Pokemon-style holographic card with cursor-driven 3D tilt, rainbow refraction, sparkle shift, and specular highlight

cardholographicpokemontilt3dhoverrainbowsparkle
Install dependencies
$npm install framer-motion
Preview

Source Code

"use client";

import { useRef, useState } from "react";
import { motion, useMotionTemplate, useMotionValue, useSpring } from "framer-motion";

interface HoloCardProps {
  children?: React.ReactNode;
  className?: string;
  /** Max tilt in degrees. Default 14. */
  tilt?: number;
  /** Holographic layer intensity 0–1. Default 0.7. */
  intensity?: number;
  /** Show sparkle texture overlay. Default true. */
  sparkles?: boolean;
}

export default function HoloCard({
  children,
  className = "",
  tilt = 14,
  intensity = 0.7,
  sparkles = true,
}: HoloCardProps) {
  const ref = useRef<HTMLDivElement>(null);
  const [hovered, setHovered] = useState(false);

  const px = useMotionValue(50);
  const py = useMotionValue(50);
  const cx = useSpring(px, { stiffness: 220, damping: 30, mass: 0.5 });
  const cy = useSpring(py, { stiffness: 220, damping: 30, mass: 0.5 });

  const rotateX = useSpring(useMotionValue(0), { stiffness: 220, damping: 26 });
  const rotateY = useSpring(useMotionValue(0), { stiffness: 220, damping: 26 });

  const handleMove = (e: React.MouseEvent<HTMLDivElement>) => {
    const el = ref.current;
    if (!el) return;
    const rect = el.getBoundingClientRect();
    const x = ((e.clientX - rect.left) / rect.width) * 100;
    const y = ((e.clientY - rect.top) / rect.height) * 100;
    px.set(x);
    py.set(y);
    rotateY.set(((x - 50) / 50) * tilt);
    rotateX.set(((50 - y) / 50) * tilt);
  };

  const handleLeave = () => {
    setHovered(false);
    px.set(50);
    py.set(50);
    rotateX.set(0);
    rotateY.set(0);
  };

  const specular = useMotionTemplate`radial-gradient(circle at ${cx}% ${cy}%, rgba(255,255,255,0.55), rgba(255,255,255,0) 40%)`;
  const holoConic = useMotionTemplate`conic-gradient(from ${cx}deg at ${cx}% ${cy}%, #ff6ec7, #ffd93d, #6dffa1, #4cc9ff, #b06bff, #ff6ec7)`;
  const sparkleShift = useMotionTemplate`${cx}% ${cy}%`;

  return (
    <motion.div
      ref={ref}
      onMouseEnter={() => setHovered(true)}
      onMouseMove={handleMove}
      onMouseLeave={handleLeave}
      style={{
        rotateX,
        rotateY,
        transformPerspective: 1200,
        transformStyle: "preserve-3d",
      }}
      className={`relative overflow-hidden rounded-2xl ${className}`}
    >
      <div className="relative z-10">{children}</div>

      {/* Holographic rainbow layer */}
      <motion.div
        className="pointer-events-none absolute inset-0 transition-opacity duration-300"
        style={{
          background: holoConic,
          mixBlendMode: "color-dodge",
          opacity: hovered ? intensity : 0,
        }}
      />

      {/* Sparkle texture — tiny dots shifted by cursor */}
      {sparkles && (
        <motion.div
          className="pointer-events-none absolute inset-0 transition-opacity duration-300"
          style={{
            opacity: hovered ? 0.55 : 0,
            backgroundImage:
              "radial-gradient(rgba(255,255,255,0.85) 1px, transparent 1px), radial-gradient(rgba(255,255,255,0.6) 1px, transparent 1px)",
            backgroundSize: "14px 14px, 22px 22px",
            backgroundPosition: sparkleShift,
            mixBlendMode: "overlay",
            filter: "blur(0.4px)",
          }}
        />
      )}

      {/* Specular highlight */}
      <motion.div
        className="pointer-events-none absolute inset-0 transition-opacity duration-300"
        style={{
          background: specular,
          opacity: hovered ? 0.9 : 0,
          mixBlendMode: "soft-light",
        }}
      />

      {/* Edge glare on tilt */}
      <div
        className="pointer-events-none absolute inset-0 rounded-2xl ring-1 ring-inset ring-white/10"
        aria-hidden="true"
      />
    </motion.div>
  );
}