Retour au catalogue
Hero 3D Marquee
Hero classique avec titre et CTA, surmonté d'un tapis de marquee en perspective 3D (rotateX 18°). Deux rows défilant en sens opposés créent un effet 'tapis roulant' vu de côté — moderne et mémorable.
heromedium Both Responsive a11y
boldminimalcorporateagencysaasportfoliocentered
Theme
"use client";
import { motion } from "framer-motion";
import { ArrowRight } from "lucide-react";
interface Hero3dMarqueeProps {
title?: string;
titleAccent?: string;
description?: string;
ctaLabel?: string;
ctaUrl?: string;
ctaSecondaryLabel?: string;
ctaSecondaryUrl?: string;
marqueeItems?: string[];
badge?: string;
}
const EASE = [0.16, 1, 0.3, 1] as const;
const SEPARATOR = "·";
function MarqueeTrack({
items,
direction,
duration,
offsetY = 0,
dimmed = false,
}: {
items: string[];
direction: 1 | -1;
duration: number;
offsetY?: number;
dimmed?: boolean;
}) {
const doubled = [...items, ...items];
return (
<div
aria-hidden
style={{
overflow: "hidden",
whiteSpace: "nowrap",
transform: `translateY(${offsetY}px)`,
marginBottom: "0.5rem",
}}
>
<motion.div
animate={{ x: direction === 1 ? ["0%", "-50%"] : ["-50%", "0%"] }}
transition={{ duration, ease: "linear", repeat: Infinity }}
style={{ display: "inline-flex", gap: "0 2.5rem", alignItems: "center" }}
>
{doubled.map((item, i) => (
<span
key={i}
style={{
display: "inline-flex",
alignItems: "center",
gap: "1.5rem",
fontSize: "clamp(1rem, 1.8vw, 1.375rem)",
fontWeight: i % 3 === 0 ? 700 : 400,
fontFamily: i % 3 === 0 ? "inherit" : "var(--font-serif)",
fontStyle: i % 3 === 0 ? "normal" : "italic",
color: dimmed
? "var(--color-foreground-muted)"
: "var(--color-foreground)",
letterSpacing: i % 3 === 0 ? "-0.02em" : "0",
opacity: dimmed ? 0.6 : 1,
whiteSpace: "nowrap",
}}
>
{item}
<span style={{ color: "var(--color-accent)", fontSize: "0.5em", opacity: 0.7 }}>
{SEPARATOR}
</span>
</span>
))}
</motion.div>
</div>
);
}
export default function Hero3dMarquee({
title = "Votre prochain projet",
titleAccent = "exceptionnel",
description = "Nous créons des expériences digitales qui marquent les esprits. Design, développement, performance.",
ctaLabel = "Démarrer un projet",
ctaUrl = "#contact",
ctaSecondaryLabel = "Voir nos réalisations",
ctaSecondaryUrl = "#portfolio",
marqueeItems = [],
badge = "Disponible maintenant",
}: Hero3dMarqueeProps) {
return (
<section
style={{
position: "relative",
overflow: "hidden",
minHeight: "100vh",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
background: "var(--color-background)",
}}
>
{/* ── Hero content ── */}
<div
style={{
position: "relative",
zIndex: 2,
width: "100%",
maxWidth: "var(--container-max-width)",
margin: "0 auto",
padding: "0 var(--container-padding-x)",
textAlign: "center",
paddingBottom: "clamp(5rem, 12vw, 9rem)",
}}
>
{/* Badge */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, ease: EASE }}
style={{
display: "inline-flex",
alignItems: "center",
gap: "0.5rem",
padding: "0.375rem 1rem",
borderRadius: "var(--radius-full)",
border: "1px solid var(--color-border)",
background: "var(--color-background-card)",
marginBottom: "2rem",
}}
>
<span
style={{
width: 6,
height: 6,
borderRadius: "50%",
background: "var(--color-accent)",
display: "inline-block",
}}
/>
<span
style={{
fontSize: "0.75rem",
letterSpacing: "0.08em",
textTransform: "uppercase",
fontWeight: 600,
color: "var(--color-foreground-muted)",
}}
>
{badge}
</span>
</motion.div>
{/* Title */}
<motion.h1
initial={{ opacity: 0, y: 28 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 0.08, ease: EASE }}
style={{
fontSize: "clamp(2.75rem, 5.5vw, 5.25rem)",
fontWeight: 800,
lineHeight: 1.04,
letterSpacing: "-0.04em",
color: "var(--color-foreground)",
marginBottom: "1.5rem",
maxWidth: "820px",
margin: "0 auto 1.5rem",
}}
>
{title}{" "}
<em
style={{
fontStyle: "italic",
fontFamily: "var(--font-serif)",
fontWeight: 400,
color: "var(--color-accent)",
}}
>
{titleAccent}
</em>
</motion.h1>
{/* Description */}
<motion.p
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.55, delay: 0.18, ease: EASE }}
style={{
fontSize: "1.125rem",
lineHeight: 1.7,
color: "var(--color-foreground-muted)",
maxWidth: "520px",
margin: "0 auto 2.5rem",
}}
>
{description}
</motion.p>
{/* CTAs */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.45, delay: 0.28, ease: EASE }}
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: "1rem",
flexWrap: "wrap",
}}
>
<a
href={ctaUrl}
style={{
display: "inline-flex",
alignItems: "center",
gap: "8px",
padding: "0.875rem 2rem",
borderRadius: "var(--radius-full)",
background: "var(--color-foreground)",
color: "var(--color-background)",
fontWeight: 600,
fontSize: "0.9375rem",
textDecoration: "none",
}}
>
{ctaLabel}
<ArrowRight style={{ width: 16, height: 16 }} />
</a>
<a
href={ctaSecondaryUrl}
style={{
display: "inline-flex",
alignItems: "center",
gap: "6px",
padding: "0.875rem 1.75rem",
borderRadius: "var(--radius-full)",
border: "1px solid var(--color-border)",
color: "var(--color-foreground-muted)",
fontWeight: 500,
fontSize: "0.9375rem",
textDecoration: "none",
background: "transparent",
}}
>
{ctaSecondaryLabel}
</a>
</motion.div>
</div>
{/* ── 3D Marquee stage ── */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.8, delay: 0.4 }}
style={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
height: "clamp(9rem, 20vw, 16rem)",
perspective: "800px",
perspectiveOrigin: "50% 0%",
overflow: "hidden",
}}
>
{/* Gradient fade — top edge */}
<div
aria-hidden
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
height: "55%",
background:
"linear-gradient(to bottom, var(--color-background) 0%, transparent 100%)",
zIndex: 1,
pointerEvents: "none",
}}
/>
{/* Gradient fade — sides */}
<div
aria-hidden
style={{
position: "absolute",
inset: 0,
background:
"linear-gradient(to right, var(--color-background) 0%, transparent 15%, transparent 85%, var(--color-background) 100%)",
zIndex: 1,
pointerEvents: "none",
}}
/>
{/* Tilted 3D wrapper */}
<div
style={{
transform: "rotateX(18deg)",
transformStyle: "preserve-3d",
paddingTop: "1rem",
}}
>
<MarqueeTrack items={marqueeItems} direction={1} duration={28} />
<MarqueeTrack
items={[...marqueeItems].reverse()}
direction={-1}
duration={22}
offsetY={4}
dimmed
/>
</div>
</motion.div>
</section>
);
}