Web InnoventixFreeCode

3D Tilt Cards

Original · free

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.

byWeb InnoventixReact + Tailwind
3d tilt cardframer motion hover cardmouse tracking cardinteractive cardtilt hover effectmotion react card
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/card-3d-tilt.json
card-3d-tilt.tsx
"use client";

import { useRef, type PointerEvent as ReactPointerEvent } from "react";
import {
  motion,
  useMotionTemplate,
  useMotionValue,
  useSpring,
  useTransform,
} from "motion/react";

const EASE = [0.16, 1, 0.3, 1] as const;

type TiltItem = {
  eyebrow: string;
  title: string;
  copy: string;
  stat: string;
  statLabel: string;
};

const ITEMS: TiltItem[] = [
  {
    eyebrow: "Analytics",
    title: "Live revenue pulse",
    copy: "Watch every signup, upgrade and renewal land in real time without refreshing a single tab.",
    stat: "+38%",
    statLabel: "vs last quarter",
  },
  {
    eyebrow: "Automation",
    title: "Hands-off workflows",
    copy: "Route work to the right person the moment a deal changes stage, day or night.",
    stat: "12h",
    statLabel: "saved weekly",
  },
  {
    eyebrow: "Insight",
    title: "Forecasts you trust",
    copy: "Blend historic trends with live pipeline so next quarter stops being a guessing game.",
    stat: "99.2%",
    statLabel: "model accuracy",
  },
];

function TiltCard({ item, index }: { item: TiltItem; index: number }) {
  const ref = useRef<HTMLElement>(null);
  const px = useMotionValue(0.5);
  const py = useMotionValue(0.5);

  const rotateX = useSpring(useTransform(py, [0, 1], [11, -11]), {
    stiffness: 150,
    damping: 16,
  });
  const rotateY = useSpring(useTransform(px, [0, 1], [-11, 11]), {
    stiffness: 150,
    damping: 16,
  });

  const glareX = useTransform(px, (v) => `${v * 100}%`);
  const glareY = useTransform(py, (v) => `${v * 100}%`);
  const glare = useMotionTemplate`radial-gradient(circle at ${glareX} ${glareY}, rgba(255,255,255,0.45), transparent 55%)`;

  function handleMove(e: ReactPointerEvent<HTMLElement>) {
    const el = ref.current;
    if (!el) return;
    const rect = el.getBoundingClientRect();
    px.set((e.clientX - rect.left) / rect.width);
    py.set((e.clientY - rect.top) / rect.height);
  }

  function handleLeave() {
    px.set(0.5);
    py.set(0.5);
  }

  return (
    <motion.div
      initial={{ opacity: 0, y: 42 }}
      whileInView={{ opacity: 1, y: 0 }}
      viewport={{ once: true, margin: "-60px" }}
      transition={{ duration: 0.6, delay: index * 0.12, ease: EASE }}
      style={{ perspective: 1100 }}
    >
      <motion.article
        ref={ref}
        onPointerMove={handleMove}
        onPointerLeave={handleLeave}
        style={{ rotateX, rotateY, transformStyle: "preserve-3d" }}
        className="group relative overflow-hidden rounded-3xl border border-zinc-200 bg-white p-7 shadow-xl shadow-zinc-900/5 transition-shadow duration-300 hover:shadow-2xl hover:shadow-indigo-500/10 dark:border-zinc-800 dark:bg-zinc-900"
      >
        <motion.div
          aria-hidden="true"
          style={{ background: glare }}
          className="pointer-events-none absolute inset-0 opacity-0 transition-opacity duration-300 group-hover:opacity-100"
        />
        <div
          aria-hidden="true"
          className="pointer-events-none absolute -right-16 -top-16 h-44 w-44 rounded-full bg-gradient-to-br from-indigo-400/40 to-violet-500/40 blur-2xl"
        />

        <div style={{ transform: "translateZ(45px)" }} className="relative">
          <span className="inline-flex items-center gap-2 rounded-full border border-zinc-200 bg-zinc-50 px-3 py-1 text-xs font-semibold uppercase tracking-widest text-indigo-600 dark:border-zinc-700 dark:bg-zinc-800 dark:text-indigo-300">
            <span className="h-1.5 w-1.5 rounded-full bg-indigo-500" />
            {item.eyebrow}
          </span>
        </div>

        <h3
          style={{ transform: "translateZ(60px)" }}
          className="relative mt-5 text-xl font-bold text-zinc-900 dark:text-white"
        >
          {item.title}
        </h3>
        <p
          style={{ transform: "translateZ(35px)" }}
          className="relative mt-2 text-sm leading-relaxed text-zinc-600 dark:text-zinc-400"
        >
          {item.copy}
        </p>

        <div
          style={{ transform: "translateZ(75px)" }}
          className="relative mt-6 flex items-end justify-between"
        >
          <div>
            <div className="text-3xl font-bold tracking-tight text-zinc-900 dark:text-white">
              {item.stat}
            </div>
            <div className="text-xs text-zinc-500 dark:text-zinc-500">
              {item.statLabel}
            </div>
          </div>
          <a
            href="#"
            aria-label={`View ${item.title}`}
            className="inline-flex h-10 w-10 items-center justify-center rounded-full bg-zinc-900 text-white shadow-lg shadow-zinc-900/20 transition-transform duration-300 group-hover:scale-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:bg-white dark:text-zinc-900 dark:focus-visible:ring-offset-zinc-900"
          >
            <svg
              viewBox="0 0 24 24"
              fill="none"
              stroke="currentColor"
              strokeWidth={2}
              strokeLinecap="round"
              strokeLinejoin="round"
              className="h-4 w-4"
              aria-hidden="true"
            >
              <path d="M7 17 17 7M7 7h10v10" />
            </svg>
          </a>
        </div>
      </motion.article>
    </motion.div>
  );
}

export default function Card3dTilt() {
  return (
    <section className="bg-zinc-50 px-6 py-20 dark:bg-zinc-950 md:py-28">
      <div className="mx-auto max-w-6xl">
        <div className="mx-auto max-w-2xl text-center">
          <motion.p
            initial={{ opacity: 0, y: 16 }}
            whileInView={{ opacity: 1, y: 0 }}
            viewport={{ once: true }}
            transition={{ duration: 0.5, ease: EASE }}
            className="text-sm font-semibold uppercase tracking-widest text-indigo-600 dark:text-indigo-400"
          >
            Interactive
          </motion.p>
          <motion.h2
            initial={{ opacity: 0, y: 20 }}
            whileInView={{ opacity: 1, y: 0 }}
            viewport={{ once: true }}
            transition={{ duration: 0.6, delay: 0.08, ease: EASE }}
            className="mt-4 text-balance text-3xl font-bold tracking-tight text-zinc-900 sm:text-4xl dark:text-white"
          >
            Cards that lean into every move
          </motion.h2>
          <motion.p
            initial={{ opacity: 0, y: 20 }}
            whileInView={{ opacity: 1, y: 0 }}
            viewport={{ once: true }}
            transition={{ duration: 0.6, delay: 0.16, ease: EASE }}
            className="mx-auto mt-4 max-w-lg text-zinc-600 dark:text-zinc-400"
          >
            Move your cursor across a card to feel the depth. Layers lift, light
            tracks the pointer and the whole thing tilts in real time.
          </motion.p>
        </div>

        <div className="mt-14 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
          {ITEMS.map((item, i) => (
            <TiltCard key={item.title} item={item} index={i} />
          ))}
        </div>
      </div>
    </section>
  );
}

Dependencies

motion

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

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 →
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.

Tilt Spotlight Card

Tilt Spotlight Card

primitives

A reusable tilt spotlight 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.