parallax /
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-tiltWhere 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.