Innovations

Mouse-driven Tilt

Cursor position translates + tilts the background. Window-into-another-room feel. Auto-disabled on touch devices.

Preview

Source

tsx
"use client";

import { useEffect, useRef } from "react";

export interface ParallaxMouseTiltProps {
  imageSrc?: string;
  /** How far the image translates with the cursor, in px. */
  range?: number;
  /** How far the image tilts, in degrees. */
  tilt?: number;
  eyebrow?: string;
  headline?: string;
  subhead?: string;
}

export default function ParallaxMouseTilt({
  imageSrc = "/heroes/cmillworks/walnut.webp",
  range = 30,
  tilt = 4,
  eyebrow = "Parallax · 04",
  headline = "Mouse-driven tilt",
  subhead =
    "The cursor moves the image. Subtle tilt + translate creates a 'window into another room' feeling. Disabled on touch devices.",
}: ParallaxMouseTiltProps) {
  const sectionRef = useRef<HTMLElement | null>(null);
  const imgRef = useRef<HTMLDivElement | null>(null);

  useEffect(() => {
    if (window.matchMedia("(hover: none)").matches) return; // skip on touch
    const el = sectionRef.current;
    const img = imgRef.current;
    if (!el || !img) return;

    let raf = 0;
    const onMove = (e: MouseEvent) => {
      const rect = el.getBoundingClientRect();
      const x = (e.clientX - rect.left) / rect.width - 0.5; // -0.5 .. 0.5
      const y = (e.clientY - rect.top) / rect.height - 0.5;
      cancelAnimationFrame(raf);
      raf = requestAnimationFrame(() => {
        img.style.transform = `translate3d(${-x * range}px, ${-y * range}px, 0) rotateX(${y * tilt}deg) rotateY(${-x * tilt}deg) scale(1.12)`;
      });
    };
    const onLeave = () => {
      cancelAnimationFrame(raf);
      img.style.transform = `translate3d(0,0,0) rotateX(0) rotateY(0) scale(1.12)`;
    };

    el.addEventListener("mousemove", onMove);
    el.addEventListener("mouseleave", onLeave);
    return () => {
      el.removeEventListener("mousemove", onMove);
      el.removeEventListener("mouseleave", onLeave);
      cancelAnimationFrame(raf);
    };
  }, [range, tilt]);

  return (
    <section
      ref={sectionRef}
      className="relative isolate flex min-h-[100svh] w-full items-center justify-center overflow-hidden bg-slate-900"
      style={{ perspective: "1200px" }}
    >
      <div
        ref={imgRef}
        className="absolute inset-0 -z-10 transition-transform duration-[600ms] ease-out will-change-transform"
        style={{
          backgroundImage: `url(${imageSrc})`,
          backgroundSize: "cover",
          backgroundPosition: "center",
          transform: "scale(1.12)",
          transformStyle: "preserve-3d",
        }}
      />
      <div className="absolute inset-0 -z-0 bg-gradient-to-b from-black/30 via-black/30 to-black/65" />
      <div className="relative z-10 mx-auto max-w-3xl px-6 text-center text-white">
        <p className="mb-4 text-xs font-semibold uppercase tracking-[0.3em] text-white/70">
          {eyebrow}
        </p>
        <h1 className="text-balance text-4xl font-semibold leading-[1.1] tracking-tight sm:text-6xl">
          {headline}
        </h1>
        <p className="mx-auto mt-6 max-w-xl text-base leading-relaxed text-white/80 sm:text-lg">
          {subhead}
        </p>
        <p className="mt-8 text-xs uppercase tracking-[0.25em] text-white/50">
          Move your cursor →
        </p>
      </div>
    </section>
  );
}
Claude Code Instructions

CLI Install

npx innovations add mouse-tilt

Where to use it

Cursor-driven, not scroll-driven. The image translates ±range/2 px and tilts ±tilt/2 deg following the cursor. The CSS transition smooths jerky movement. AUTO-DISABLED on touch devices (matchMedia '(hover: none)'). The image stays put with no listener attached. WHEN TO USE: - Portfolio / showcase pages where you want interactivity - Product hero where the image rewards exploration (rich texture, product photography) - Avoid on info-dense pages — it pulls focus TUNING: - range (default 30px) — total pixel travel - tilt (default 4°) — how much 3D rotation - The image is scaled 1.12 to hide edges revealed by the tilt PERFORMANCE: requestAnimationFrame-throttled, GPU transforms. Cost is only paid while the cursor is over the section.