Web InnoventixFreeCode

Tilt 3d Effect

Original · free

Image tilts in 3D toward the cursor on hover.

byWeb InnoventixReact + Tailwind
tilt3dimageeffect
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/imgfx-tilt-3d.json
imgfx-tilt-3d.tsx
"use client";

import { useRef, useState, type PointerEvent } from "react";
import {
  motion,
  useMotionValue,
  useSpring,
  useTransform,
  useReducedMotion,
  type MotionStyle,
} from "motion/react";

export default function ImgfxTilt3D() {
  const reduceMotion = useReducedMotion();
  const cardRef = useRef<HTMLDivElement>(null);
  const [active, setActive] = useState(false);

  // Cursor position mapped to -0.5 .. 0.5 across the card.
  const px = useMotionValue(0);
  const py = useMotionValue(0);

  const springCfg = { stiffness: 220, damping: 20, mass: 0.6 } as const;
  const sx = useSpring(px, springCfg);
  const sy = useSpring(py, springCfg);

  const MAX_TILT = 14;
  const rotateX = useTransform(sy, [-0.5, 0.5], [MAX_TILT, -MAX_TILT]);
  const rotateY = useTransform(sx, [-0.5, 0.5], [-MAX_TILT, MAX_TILT]);

  // Glare + parallax follow the cursor.
  const glareX = useTransform(sx, [-0.5, 0.5], ["18%", "82%"]);
  const glareY = useTransform(sy, [-0.5, 0.5], ["18%", "82%"]);
  const imgShiftX = useTransform(sx, [-0.5, 0.5], ["3%", "-3%"]);
  const imgShiftY = useTransform(sy, [-0.5, 0.5], ["3%", "-3%"]);

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

  function handleLeave() {
    px.set(0);
    py.set(0);
    setActive(false);
  }

  const cardStyle: MotionStyle = reduceMotion
    ? {}
    : { rotateX, rotateY, transformStyle: "preserve-3d" };

  return (
    <section className="relative w-full overflow-hidden bg-slate-50 px-6 py-20 dark:bg-neutral-950 sm:px-10 sm:py-28">
      {/* soft ambient backdrop */}
      <div
        aria-hidden
        className="pointer-events-none absolute inset-0 bg-[radial-gradient(60%_50%_at_50%_0%,theme(colors.indigo.200/.55),transparent_70%)] dark:bg-[radial-gradient(60%_50%_at_50%_0%,theme(colors.indigo.500/.16),transparent_70%)]"
      />

      <div className="relative mx-auto flex max-w-3xl flex-col items-center text-center">
        <span className="inline-flex items-center gap-2 rounded-full border border-indigo-200 bg-white/70 px-3 py-1 text-xs font-medium uppercase tracking-widest text-indigo-600 backdrop-blur dark:border-indigo-400/20 dark:bg-white/5 dark:text-indigo-300">
          <span className="h-1.5 w-1.5 rounded-full bg-indigo-500" />
          3D Tilt
        </span>

        <h2 className="mt-5 text-balance text-3xl font-semibold tracking-tight text-slate-900 dark:text-white sm:text-4xl">
          The image leans toward your cursor
        </h2>
        <p className="mt-3 max-w-md text-pretty text-sm text-slate-500 dark:text-neutral-400">
          Move your pointer across the frame &mdash; depth, parallax and a
          travelling highlight react in real time.
        </p>

        {/* Perspective stage */}
        <div
          className="mt-12 w-full max-w-md"
          style={{ perspective: reduceMotion ? undefined : "1100px" }}
        >
          <motion.div
            ref={cardRef}
            onPointerMove={handleMove}
            onPointerEnter={() => setActive(true)}
            onPointerLeave={handleLeave}
            style={cardStyle}
            className="group relative aspect-[4/5] w-full select-none rounded-3xl"
          >
            {/* base shadow that deepens on hover */}
            <div
              aria-hidden
              className="absolute inset-0 rounded-3xl shadow-xl shadow-slate-900/10 transition-shadow duration-300 group-hover:shadow-2xl group-hover:shadow-indigo-900/25 dark:shadow-black/40"
            />

            {/* the picture */}
            <div className="absolute inset-0 overflow-hidden rounded-3xl ring-1 ring-slate-900/10 dark:ring-white/10">
              <motion.div
                className="absolute inset-[-6%]"
                style={
                  reduceMotion ? {} : { x: imgShiftX, y: imgShiftY }
                }
              >
                {/* eslint-disable-next-line @next/next/no-img-element */}
                <img
                  src="/img/gallery/g07.webp"
                  alt="A photograph tilting in three dimensions toward the pointer"
                  loading="lazy"
                  draggable={false}
                  className="h-full w-full scale-110 object-cover"
                />
              </motion.div>

              {/* moving glare */}
              <motion.div
                aria-hidden
                className="pointer-events-none absolute inset-0 mix-blend-soft-light"
                style={{
                  opacity: active && !reduceMotion ? 1 : 0,
                  ["--gx" as string]: glareX,
                  ["--gy" as string]: glareY,
                  backgroundImage:
                    "radial-gradient(28% 28% at var(--gx) var(--gy), rgba(255,255,255,0.85), transparent 60%)",
                  transition: "opacity 250ms ease",
                }}
              />

              {/* inner vignette for contrast */}
              <div
                aria-hidden
                className="pointer-events-none absolute inset-0 rounded-3xl bg-[radial-gradient(120%_120%_at_50%_120%,rgba(2,6,23,0.35),transparent_55%)] opacity-70 dark:opacity-90"
              />
            </div>

            {/* floating caption lifted off the surface */}
            <motion.div
              style={
                reduceMotion
                  ? {}
                  : { transform: "translateZ(60px)", transformStyle: "preserve-3d" }
              }
              className="absolute inset-x-4 bottom-4 flex items-center justify-between rounded-2xl border border-white/20 bg-white/10 px-4 py-2.5 backdrop-blur-md"
            >
              <span className="text-sm font-medium text-white drop-shadow">
                Northlight, 06:14
              </span>
              <span className="rounded-full bg-white/85 px-2.5 py-0.5 text-[11px] font-semibold text-slate-900">
                hover me
              </span>
            </motion.div>
          </motion.div>
        </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 →