Cinematic Heroes · Intermediate
Layered Depth Hero
Normalised pointer position drives a shared spring; each layer multiplies it by a different depth factor.
Live preview
Move a mouse across the hero to separate its layers.
Copy-ready · type-checkedThe preview above renders this exact file. Reduced-motion fallback included.
/**
* Layered Hero
* Three spring-smoothed planes create a restrained editorial parallax.
* Requires: motion, Tailwind with shadcn semantic tokens.
*/
import { useEffect } from "react";
import { motion, useReducedMotion, useSpring, useTransform } from "motion/react";
export default function LayeredHero({
reduced: reducedProp,
intensity = 1,
}: {
reduced?: boolean;
intensity?: number;
}) {
const systemReduced = useReducedMotion();
const reduced = reducedProp ?? systemReduced ?? false;
const amount = Math.min(1.5, Math.max(0.5, intensity));
const x = useSpring(0, { stiffness: 65, damping: 22 });
const y = useSpring(0, { stiffness: 65, damping: 22 });
const backX = useTransform(x, (value) => value * 12);
const backY = useTransform(y, (value) => value * 12);
const titleX = useTransform(x, (value) => value * 26);
const titleY = useTransform(y, (value) => value * 26);
const frontX = useTransform(x, (value) => value * 48);
const frontY = useTransform(y, (value) => value * 48);
useEffect(() => {
if (reduced) {
x.jump(0);
y.jump(0);
}
}, [reduced, x, y]);
function reset() {
x.set(0);
y.set(0);
}
return (
<section
className="relative isolate flex min-h-[80vh] items-center justify-center overflow-hidden bg-background px-6 py-24 text-foreground"
onPointerLeave={reset}
onPointerCancel={reset}
onPointerMove={(event) => {
if (
reduced ||
event.pointerType !== "mouse" ||
!window.matchMedia("(hover: hover) and (pointer: fine)").matches
)
return;
const rect = event.currentTarget.getBoundingClientRect();
x.set(((event.clientX - rect.left) / rect.width - 0.5) * amount);
y.set(((event.clientY - rect.top) / rect.height - 0.5) * amount);
}}
>
<motion.img
src="https://picsum.photos/seed/ardent-horizon/1920/1200"
alt=""
style={{ x: reduced ? 0 : backX, y: reduced ? 0 : backY }}
className="pointer-events-none absolute inset-[-5%] -z-20 h-[110%] w-[110%] object-cover opacity-25"
/>
<div className="absolute inset-0 -z-10 bg-background/50" />
<motion.div
style={{ x: reduced ? 0 : titleX, y: reduced ? 0 : titleY }}
className="relative z-10 text-center"
>
<p className="mb-8 font-mono text-xs uppercase tracking-[0.2em] text-muted-foreground">
Beyond the familiar / 11
</p>
<h2 className="font-serif text-[clamp(4rem,14vw,11rem)] leading-none tracking-tight">
Horizon
</h2>
<p className="mx-auto mt-8 max-w-xs text-sm leading-relaxed text-muted-foreground">
A new perspective begins with the smallest shift.
</p>
</motion.div>
<motion.div
aria-hidden="true"
style={{ x: reduced ? 0 : frontX, y: reduced ? 0 : frontY }}
className="pointer-events-none absolute bottom-[5%] right-[5%] h-28 w-20 overflow-hidden rounded-lg border border-border sm:bottom-[10%] sm:right-[10%] sm:h-48 sm:w-36"
>
<img
src="https://picsum.photos/seed/ardent-detail/400/600"
alt=""
className="h-full w-full object-cover"
/>
</motion.div>
<p className="absolute bottom-6 left-6 font-mono text-xs uppercase tracking-[0.2em] text-muted-foreground">
{reduced ? "A study in depth" : "Three planes / one perspective"}
</p>
</section>
);
}
Next patternReading Progress Indicator→