Tilting Card
This is a 3d card component built using nextjs
lucide-reactframer-motion
Tilted Project
Mouse-tracked 3D tilt with spotlight glow and shimmer border.
Hover to tilt
Installation
Install the required dependencies:
npm install lucide-react framer-motionlib/utils.ts
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
components/ui/tilting-card.tsx
"use client";
import React, { useRef, useState } from "react";
import { motion, useMotionValue, useTransform, useSpring } from "framer-motion";
import { cn } from "@/lib/utils";
type CardProps = {
title?: string;
description?: string;
badge?: string;
motionOff?: boolean;
className?: string;
children?: React.ReactNode;
glowIntensity?: "low" | "medium" | "high";
};
export function CardTilt({
title,
description,
badge,
motionOff = false,
className,
children,
glowIntensity = "medium",
}: CardProps) {
const cardRef = useRef<HTMLDivElement>(null);
const [isHovered, setIsHovered] = useState(false);
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
const rawX = useMotionValue(0);
const rawY = useMotionValue(0);
const rotateX = useSpring(useTransform(rawY, [-1, 1], [14, -14]), {
stiffness: 260,
damping: 22,
});
const rotateY = useSpring(useTransform(rawX, [-1, 1], [-14, 14]), {
stiffness: 260,
damping: 22,
});
const rotateZ = useSpring(useTransform(rawX, [-1, 1], [-1.5, 1.5]), {
stiffness: 200,
damping: 30,
});
const scale = useSpring(isHovered && !motionOff ? 1.04 : 1, {
stiffness: 300,
damping: 25,
});
const glowOpacity = {
low: 0.25,
medium: 0.45,
high: 0.65,
}[glowIntensity];
function handleMouseMove(e: React.MouseEvent<HTMLDivElement>) {
if (motionOff || !cardRef.current) return;
const rect = cardRef.current.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
const y = ((e.clientY - rect.top) / rect.height) * 2 - 1;
rawX.set(x);
rawY.set(y);
setMousePos({
x: ((e.clientX - rect.left) / rect.width) * 100,
y: ((e.clientY - rect.top) / rect.height) * 100,
});
}
function handleMouseLeave() {
rawX.set(0);
rawY.set(0);
setIsHovered(false);
}
return (
<motion.div
ref={cardRef}
style={
!motionOff
? { rotateX, rotateY, rotateZ, scale, transformPerspective: 900 }
: {}
}
onMouseMove={handleMouseMove}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={handleMouseLeave}
className={cn(
"relative flex justify-center items-center cursor-pointer select-none",
"rounded-3xl p-[10px] ring-5 ring-neutral-300 shadow-[inset_0_2px_4px_rgba(0,0,0,0.3)] dark:shadow-[inset_0_2px_4px_rgba(255,255,255,0.3)]",
" overflow-hidden",
className,
)}
>
<motion.div
className={cn(
"pointer-events-none absolute w-80 h-80 rounded-full -translate-x-1/2 blur-3xl -translate-y-1/2 -z-50",
"bg-[radial-gradient(circle,_rgba(52,211,153,0.26)_0%,_rgba(16,185,129,0.16)_50%,_rgba(6,95,70,0.08)_100%)]",
"dark:bg-[radial-gradient(circle,_rgba(52,211,153,0.15)_0%,_rgba(16,185,129,0.8)_50%,_rgba(6,95,70,0.04)_100%)]",
)}
animate={{ opacity: isHovered ? 1 : 0 }}
transition={{ type: "spring", stiffness: 350, damping: 28 }}
style={{
left: `${mousePos.x}%`,
top: `${mousePos.y}%`,
}}
/>
<div
className={cn(
"relative overflow-hidden w-full rounded-[22px] z-10",
"bg-white dark:bg-zinc-900",
"bg-zinc-100 dark:bg-zinc-800 shadow-[0_115px_5px_rgba(0,0,0,0.1),0_2px_5px_rgba(0,0,0,0.1)] dark:shadow-[0_115px_50px_rgba(255,255,255,0.2),0_5px_55px_rgba(255,255,255,0.2)]",
)}
>
<div
className="absolute inset-0 z-10 opacity-[0.035] dark:opacity-[0.06] pointer-events-none rounded-[22px]"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E")`,
backgroundSize: "128px 128px",
}}
/>
<motion.div
className="absolute top-0 left-8 right-8 h-px z-20"
style={{
background:
"linear-gradient(to right, transparent, rgba(150,150,150,0.6), transparent)",
}}
animate={
isHovered && !motionOff
? { opacity: [0.4, 1, 0.4], scaleX: [0.7, 1, 0.7] }
: { opacity: 0.4, scaleX: 0.7 }
}
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
/>
<div className="relative z-30 p-7">
{badge && (
<motion.span
initial={{ opacity: 0, y: -6 }}
animate={{ opacity: 1, y: 0 }}
className={cn(
"inline-block mb-4 px-3 py-1 text-[10px] font-semibold tracking-[0.2em] uppercase rounded-full",
"bg-zinc-100 text-zinc-500 border border-zinc-200",
"dark:bg-zinc-800 dark:text-zinc-400 dark:border-zinc-700",
)}
>
{badge}
</motion.span>
)}
{children ? (
<>{children}</>
) : (
<>
<motion.div
className={cn(
"w-10 h-10 mb-5 rounded-xl flex items-center justify-center",
"bg-zinc-100 dark:bg-zinc-800",
"border border-zinc-200 dark:border-zinc-700",
)}
animate={
isHovered && !motionOff
? { rotate: [0, -8, 8, 0] }
: { rotate: 0 }
}
transition={{ duration: 0.6, ease: "easeInOut" }}
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
className="text-zinc-500 dark:text-zinc-400"
stroke="currentColor"
strokeWidth="1.5"
>
<rect x="3" y="3" width="7" height="7" rx="1" />
<rect x="14" y="3" width="7" height="7" rx="1" />
<rect x="3" y="14" width="7" height="7" rx="1" />
<rect x="14" y="14" width="7" height="7" rx="1" />
</svg>
</motion.div>
<h3
className={cn(
"text-[1.2rem] font-bold tracking-tight mb-2 leading-snug",
"text-zinc-900 dark:text-zinc-50",
)}
>
{title || "Tilted Project"}
</h3>
<p
className={cn(
"text-sm leading-relaxed",
"text-zinc-500 dark:text-zinc-400",
)}
>
{description ||
"Mouse-tracked 3D tilt with spotlight glow and shimmer border."}
</p>
<div
className={cn(
"mt-6 pt-4 flex items-center justify-between",
"border-t border-zinc-100 dark:border-zinc-800",
)}
>
<span className="text-[11px] tracking-widest uppercase text-zinc-400 dark:text-zinc-600 font-medium">
Hover to tilt
</span>
<motion.div
className={cn(
"w-7 h-7 rounded-full flex items-center justify-center",
"bg-zinc-900 dark:bg-zinc-100",
)}
animate={
isHovered && !motionOff
? { scale: 1.18, rotate: 45 }
: { scale: 1, rotate: 0 }
}
transition={{ type: "spring", stiffness: 300, damping: 18 }}
>
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
className="text-white dark:text-zinc-900"
>
<path d="M5 12h14M12 5l7 7-7 7" />
</svg>
</motion.div>
</div>
</>
)}
</div>
<motion.div
className="absolute bottom-0 left-1/2 -translate-x-1/2 h-px w-3/4 pointer-events-none z-20"
style={{
background:
"linear-gradient(to right, transparent, rgba(120,120,120,0.5), transparent)",
}}
animate={
isHovered && !motionOff
? { opacity: 1, scaleX: 1 }
: { opacity: 0, scaleX: 0.4 }
}
transition={{ duration: 0.4 }}
/>
</div>
</motion.div>
);
}