Retour au catalogue
Hero 3D Tilt
Hero avec carte 3D qui s'incline au survol de la souris. Effet perspective immersif.
heromedium Both Responsive a11y
boldplayfulsaasagencycentered
Theme
"use client";
import { motion, useMotionValue, useSpring, useTransform } from "framer-motion";
import { ArrowRight } from "lucide-react";
import { useRef } from "react";
interface Hero3dProps {
title?: string;
titleAccent?: string;
description?: string;
ctaLabel?: string;
ctaUrl?: string;
badge?: string;
}
const EASE = [0.16, 1, 0.3, 1] as const;
export default function Hero3d({
title = "Votre titre principal",
titleAccent = "innovant",
description = "Description du hero",
ctaLabel = "Commencer",
ctaUrl = "#contact",
badge = "",
}: Hero3dProps) {
const cardRef = useRef<HTMLDivElement>(null);
const mouseX = useMotionValue(0);
const mouseY = useMotionValue(0);
const rotateX = useSpring(useTransform(mouseY, [-0.5, 0.5], [8, -8]), {
stiffness: 200,
damping: 30,
});
const rotateY = useSpring(useTransform(mouseX, [-0.5, 0.5], [-8, 8]), {
stiffness: 200,
damping: 30,
});
function handleMouseMove(e: React.MouseEvent<HTMLDivElement>) {
const rect = cardRef.current?.getBoundingClientRect();
if (!rect) return;
const x = (e.clientX - rect.left) / rect.width - 0.5;
const y = (e.clientY - rect.top) / rect.height - 0.5;
mouseX.set(x);
mouseY.set(y);
}
function handleMouseLeave() {
mouseX.set(0);
mouseY.set(0);
}
return (
<section
className="relative overflow-hidden flex items-center justify-center"
style={{
paddingTop: "var(--section-padding-y-lg)",
paddingBottom: "var(--section-padding-y-lg)",
background: "var(--color-background)",
minHeight: "85vh",
perspective: "1200px",
}}
>
<div
className="mx-auto relative z-10"
style={{
maxWidth: "var(--container-max-width)",
paddingLeft: "var(--container-padding-x)",
paddingRight: "var(--container-padding-x)",
width: "100%",
}}
>
<motion.div
ref={cardRef}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
style={{ rotateX, rotateY, transformStyle: "preserve-3d" }}
className="relative rounded-2xl p-12 md:p-16 text-center"
>
{/* Glass background */}
<div
aria-hidden
className="absolute inset-0 rounded-2xl"
style={{
background: "var(--color-background-alt)",
border: "1px solid var(--color-border)",
borderRadius: "var(--radius-xl)",
}}
/>
{/* Accent glow */}
<div
aria-hidden
className="absolute -top-20 left-1/2 -translate-x-1/2 w-80 h-80 rounded-full pointer-events-none"
style={{
background: "var(--color-accent)",
opacity: 0.08,
filter: "blur(80px)",
}}
/>
<div className="relative z-10">
{badge && (
<motion.span
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, ease: EASE }}
className="inline-block px-4 py-1.5 rounded-full text-xs font-medium mb-6"
style={{
border: "1px solid var(--color-accent-border)",
background: "var(--color-accent-subtle)",
color: "var(--color-foreground-muted)",
}}
>
{badge}
</motion.span>
)}
<motion.h1
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.06, ease: EASE }}
className="font-bold tracking-tight mb-6"
style={{
fontFamily: "var(--font-sans)",
fontSize: "clamp(2.25rem, 4.5vw, 4rem)",
lineHeight: 1.1,
color: "var(--color-foreground)",
}}
>
{title}{" "}
<em
style={{
fontFamily: "var(--font-serif)",
fontStyle: "italic",
fontWeight: 400,
color: "var(--color-accent)",
}}
>
{titleAccent}
</em>
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.14, ease: EASE }}
className="text-base leading-relaxed max-w-md mx-auto mb-8"
style={{ color: "var(--color-foreground-muted)" }}
>
{description}
</motion.p>
<motion.a
href={ctaUrl}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.45, delay: 0.22, ease: EASE }}
className="inline-flex items-center gap-2 px-7 py-3 rounded-full text-sm font-semibold"
style={{
background: "var(--color-accent)",
color: "var(--color-foreground)",
textDecoration: "none",
}}
>
{ctaLabel}
<ArrowRight className="h-4 w-4" />
</motion.a>
</div>
</motion.div>
</div>
</section>
);
}