Tilt Spotlight Card
primitives · attributedA reusable tilt spotlight card for React and Tailwind, with hover and motion.
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/src-tilt-spotlight-card.json/* Source: motion-primitives (Julien Thibeaut / ibelick) — https://github.com/ibelick/motion-primitives (components/core/tilt.tsx + components/core/spotlight.tsx + app/docs/tilt/tilt-spotlight.tsx) — MIT. Included under its original licence.
Copyright (c) 2024 ibelick. Adapted: merged Tilt + Spotlight into one self-contained file, dropped the cn/@/lib/utils helper, and replaced the remote demo image with an inline gradient/SVG artwork so nothing is hotlinked. */
"use client";
import { useRef, useState, useCallback, useEffect } from "react";
import {
motion,
useMotionTemplate,
useMotionValue,
useSpring,
useTransform,
type SpringOptions,
type MotionStyle,
} from "motion/react";
type TiltProps = {
children: React.ReactNode;
className?: string;
style?: MotionStyle;
rotationFactor?: number;
isReverse?: boolean;
springOptions?: SpringOptions;
};
function Tilt({
children,
className,
style,
rotationFactor = 15,
isReverse = false,
springOptions,
}: TiltProps) {
const ref = useRef<HTMLDivElement>(null);
const x = useMotionValue(0);
const y = useMotionValue(0);
const xSpring = useSpring(x, springOptions);
const ySpring = useSpring(y, springOptions);
const rotateX = useTransform(
ySpring,
[-0.5, 0.5],
isReverse ? [rotationFactor, -rotationFactor] : [-rotationFactor, rotationFactor]
);
const rotateY = useTransform(
xSpring,
[-0.5, 0.5],
isReverse ? [-rotationFactor, rotationFactor] : [rotationFactor, -rotationFactor]
);
const transform = useMotionTemplate`perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`;
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
if (!ref.current) return;
const rect = ref.current.getBoundingClientRect();
const xPos = (e.clientX - rect.left) / rect.width - 0.5;
const yPos = (e.clientY - rect.top) / rect.height - 0.5;
x.set(xPos);
y.set(yPos);
};
const handleMouseLeave = () => {
x.set(0);
y.set(0);
};
return (
<motion.div
ref={ref}
className={className}
style={{ transformStyle: "preserve-3d", ...style, transform }}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
>
{children}
</motion.div>
);
}
type SpotlightProps = {
className?: string;
size?: number;
springOptions?: SpringOptions;
};
function Spotlight({
className,
size = 200,
springOptions = { bounce: 0 },
}: SpotlightProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [isHovered, setIsHovered] = useState(false);
const [parentElement, setParentElement] = useState<HTMLElement | null>(null);
const mouseX = useSpring(0, springOptions);
const mouseY = useSpring(0, springOptions);
const spotlightLeft = useTransform(mouseX, (v) => `${v - size / 2}px`);
const spotlightTop = useTransform(mouseY, (v) => `${v - size / 2}px`);
useEffect(() => {
if (containerRef.current) {
const parent = containerRef.current.parentElement;
if (parent) {
parent.style.position = "relative";
parent.style.overflow = "hidden";
setParentElement(parent);
}
}
}, []);
const handleMouseMove = useCallback(
(event: MouseEvent) => {
if (!parentElement) return;
const { left, top } = parentElement.getBoundingClientRect();
mouseX.set(event.clientX - left);
mouseY.set(event.clientY - top);
},
[mouseX, mouseY, parentElement]
);
useEffect(() => {
if (!parentElement) return;
const controller = new AbortController();
parentElement.addEventListener("mousemove", handleMouseMove, {
signal: controller.signal,
});
parentElement.addEventListener("mouseenter", () => setIsHovered(true), {
signal: controller.signal,
});
parentElement.addEventListener("mouseleave", () => setIsHovered(false), {
signal: controller.signal,
});
return () => controller.abort();
}, [parentElement, handleMouseMove]);
return (
<motion.div
ref={containerRef}
className={[
"pointer-events-none absolute rounded-full blur-2xl transition-opacity duration-200",
isHovered ? "opacity-100" : "opacity-0",
className || "",
].join(" ")}
style={{
width: size,
height: size,
left: spotlightLeft,
top: spotlightTop,
background:
"radial-gradient(circle at center, rgba(255,255,255,0.6), rgba(255,255,255,0.15), transparent 80%)",
}}
/>
);
}
export default function TiltSpotlightCard() {
return (
<section className="bg-white px-6 py-16 dark:bg-zinc-950 md:py-24">
<div className="mx-auto max-w-2xl text-center">
<p className="text-sm font-semibold uppercase tracking-widest text-indigo-600 dark:text-indigo-400">
Interactive
</p>
<h2 className="mt-3 text-balance text-3xl font-bold tracking-tight text-zinc-900 sm:text-4xl dark:text-white">
A card that tilts toward you
</h2>
<p className="mt-4 text-lg text-zinc-600 dark:text-zinc-400">
Hover to tip the card in 3D while a soft spotlight glides across the
artwork.
</p>
</div>
<div className="mx-auto mt-12 max-w-sm">
<Tilt
rotationFactor={8}
isReverse
style={{ transformOrigin: "center center" }}
springOptions={{ stiffness: 26.7, damping: 4.1, mass: 0.2 }}
className="group relative rounded-2xl border border-zinc-200 bg-white p-2 shadow-sm dark:border-zinc-800 dark:bg-zinc-900"
>
<Spotlight size={260} />
<div className="relative h-40 overflow-hidden rounded-xl bg-gradient-to-br from-indigo-500 via-violet-500 to-fuchsia-500 grayscale transition duration-700 group-hover:grayscale-0">
<svg
aria-hidden="true"
viewBox="0 0 400 200"
className="h-full w-full opacity-90"
preserveAspectRatio="none"
>
<defs>
<linearGradient id="tilt-stroke" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stopColor="rgba(255,255,255,0.9)" />
<stop offset="100%" stopColor="rgba(255,255,255,0.2)" />
</linearGradient>
</defs>
<g fill="none" stroke="url(#tilt-stroke)" strokeWidth="2">
<path d="M0 150 Q 100 60 200 120 T 400 90" />
<path d="M0 170 Q 120 100 200 150 T 400 130" opacity="0.6" />
<circle cx="300" cy="70" r="26" />
</g>
</svg>
</div>
<div className="relative flex flex-col gap-0.5 px-3 pb-3 pt-4">
<span className="font-mono text-xs font-medium uppercase tracking-wide text-zinc-500 dark:text-zinc-400">
Case study
</span>
<h3 className="text-base font-semibold text-zinc-900 dark:text-white">
Rebuilding a checkout in a weekend
</h3>
</div>
</Tilt>
</div>
</section>
);
}Dependencies
Licence
motion-primitives (Julien Thibeaut / ibelick) (original) · Licensed under primitives.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
3D Tilt Cards
OriginalA responsive grid of cards that tilt in 3D toward your cursor with a light glare and lifted depth layers, powered by Framer Motion springs.

Spotlight Follow Cards
OriginalFeature cards where a soft radial spotlight and a glowing border chase the cursor on hover, with a staggered scroll reveal and an animated shimmer line.

3D Flip Cards
OriginalPricing-style cards that flip in 3D on hover or tap to reveal details on the back, keyboard accessible with a staggered entrance animation.

Animated Gradient Border Cards
OriginalCards wrapped in a live conic gradient border that rotates and speeds up on hover, with a shimmer sweep and a scrolling tag marquee.

Glow Effect Card
primitivesA reusable glow effect card for React and Tailwind, with hover and motion.

Neon Gradient Card
gradient-card.tsxA reusable neon gradient card for React and Tailwind, with hover and motion.

Spotlight Magic Card
card.tsxA reusable spotlight magic card for React and Tailwind, with hover and motion.

Spotlight Hero
OriginalA centred hero with a soft radial spotlight, badge and dual call-to-action.

Split Hero
OriginalA two-column hero pairing a headline and CTAs with a product mock and social proof.

Gradient Spotlight Hero
OriginalA minimal centred hero with a soft gradient-mesh backdrop, announcement pill and dual call-to-action buttons.

App Preview Hero
OriginalA centred hero that pairs headline copy with a realistic product dashboard mock built entirely from markup, complete with browser chrome and a floating notification card.

Waitlist Capture Hero
OriginalA dark, focused hero with an inline email capture form and avatar social proof, ready for pre-launch waitlists.

