Web InnoventixFreeCode

Tilt Spotlight Card

primitives · attributed

A reusable tilt spotlight card for React and Tailwind, with hover and motion.

tiltspotlightcardcards
Open

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
src-tilt-spotlight-card.tsx
/* 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

motion

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 quote

Similar components

Browse all →
3D Tilt Cards

3D Tilt Cards

Original

A 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

Spotlight Follow Cards

Original

Feature 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

3D Flip Cards

Original

Pricing-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

Animated Gradient Border Cards

Original

Cards 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

Glow Effect Card

primitives

A reusable glow effect card for React and Tailwind, with hover and motion.

Neon Gradient Card

Neon Gradient Card

gradient-card.tsx

A reusable neon gradient card for React and Tailwind, with hover and motion.

Spotlight Magic Card

Spotlight Magic Card

card.tsx

A reusable spotlight magic card for React and Tailwind, with hover and motion.

Spotlight Hero

Spotlight Hero

Original

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

Split Hero

Split Hero

Original

A two-column hero pairing a headline and CTAs with a product mock and social proof.

Gradient Spotlight Hero

Gradient Spotlight Hero

Original

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

App Preview Hero

App Preview Hero

Original

A 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

Waitlist Capture Hero

Original

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