Retour au catalogue
Footer Animated Reveal
Footer premium avec un reveal theatral du nom de marque en grand (clip-mask, 10vw) au whileInView. Colonnes de liens en stagger, separateur qui se dessine (scaleX), icones sociales avec rotation au hover. Gradient aurora subtil en fond.
footermedium Both Responsive a11y
minimalelegantboldsaasagencyportfoliostacked
Theme
"use client";
import { useRef } from "react";
import { motion, useInView } from "framer-motion";
interface FooterColumn {
title: string;
links: { label: string; href: string }[];
}
interface SocialLink {
label: string;
href: string;
}
interface FooterAnimatedRevealProps {
brandName?: string;
tagline?: string;
columns?: FooterColumn[];
socialLinks?: SocialLink[];
copyright?: string;
}
const EASE = [0.16, 1, 0.3, 1] as const;
function SocialPill({ label, href }: SocialLink) {
return (
<motion.a
href={href}
whileHover={{ rotate: -6, scale: 1.08 }}
whileTap={{ scale: 0.94 }}
transition={{ type: "spring", stiffness: 380, damping: 18 }}
style={{
display: "inline-flex",
alignItems: "center",
padding: "0.375rem 1rem",
borderRadius: "var(--radius-full)",
border: "1px solid var(--color-border)",
fontSize: "0.8125rem",
fontWeight: 500,
color: "var(--color-foreground-muted)",
textDecoration: "none",
letterSpacing: "0.01em",
}}
>
{label}
</motion.a>
);
}
export default function FooterAnimatedReveal({
brandName = "Studio",
tagline = "Crafting digital experiences",
columns = [],
socialLinks = [],
copyright = "© 2026 Studio. All rights reserved.",
}: FooterAnimatedRevealProps) {
const ref = useRef<HTMLElement>(null);
const inView = useInView(ref, { once: true, margin: "-80px" });
return (
<footer
ref={ref}
style={{
position: "relative",
overflow: "hidden",
background: "var(--color-background-card, var(--color-background))",
paddingTop: "5rem",
paddingBottom: "2.5rem",
}}
>
{/* Aurora gradient background */}
<div
aria-hidden
style={{
position: "absolute",
inset: 0,
zIndex: 0,
background:
"radial-gradient(ellipse 80% 50% at 20% 110%, color-mix(in srgb, var(--color-accent) 12%, transparent), transparent 60%), radial-gradient(ellipse 60% 40% at 80% 100%, color-mix(in srgb, var(--color-accent) 7%, transparent), transparent 60%)",
pointerEvents: "none",
}}
/>
<div
style={{
position: "relative",
zIndex: 1,
maxWidth: "var(--container-max-width)",
margin: "0 auto",
padding: "0 var(--container-padding-x)",
}}
>
{/* Brand name — clip-mask reveal */}
<div style={{ overflow: "hidden", marginBottom: "1.25rem" }}>
<motion.h2
initial={{ y: "110%" }}
animate={inView ? { y: "0%" } : {}}
transition={{ duration: 0.9, ease: EASE }}
style={{
fontSize: "clamp(3.5rem, 10vw, 9rem)",
fontWeight: 800,
lineHeight: 0.9,
letterSpacing: "-0.04em",
color: "var(--color-foreground)",
margin: 0,
fontFamily: "var(--font-sans)",
}}
>
{brandName}
</motion.h2>
</div>
{/* Tagline reveal */}
<div style={{ overflow: "hidden", marginBottom: "3.5rem" }}>
<motion.p
initial={{ y: "100%", opacity: 0 }}
animate={inView ? { y: "0%", opacity: 1 } : {}}
transition={{ duration: 0.7, delay: 0.15, ease: EASE }}
style={{
fontSize: "1rem",
color: "var(--color-foreground-muted)",
margin: 0,
letterSpacing: "0.01em",
}}
>
{tagline}
</motion.p>
</div>
{/* Link columns — stagger */}
{columns.length > 0 && (
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(140px, 1fr))",
gap: "2.5rem",
marginBottom: "3.5rem",
}}
>
{columns.map((col, i) => (
<motion.div
key={col.title}
initial={{ opacity: 0, y: 20 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.55, delay: 0.25 + i * 0.08, ease: EASE }}
>
<p
style={{
fontSize: "0.6875rem",
fontWeight: 600,
textTransform: "uppercase",
letterSpacing: "0.1em",
color: "var(--color-foreground-muted)",
marginBottom: "1rem",
marginTop: 0,
}}
>
{col.title}
</p>
<ul style={{ listStyle: "none", margin: 0, padding: 0, display: "flex", flexDirection: "column", gap: "0.625rem" }}>
{col.links.map((link) => (
<li key={link.href}>
<a
href={link.href}
style={{
fontSize: "0.875rem",
color: "var(--color-foreground-muted)",
textDecoration: "none",
transition: "color 0.2s",
}}
onMouseEnter={(e) => {
(e.currentTarget as HTMLAnchorElement).style.color = "var(--color-foreground)";
}}
onMouseLeave={(e) => {
(e.currentTarget as HTMLAnchorElement).style.color = "var(--color-foreground-muted)";
}}
>
{link.label}
</a>
</li>
))}
</ul>
</motion.div>
))}
</div>
)}
{/* Separator — scaleX draw */}
<div style={{ overflow: "hidden", marginBottom: "1.5rem" }}>
<motion.div
initial={{ scaleX: 0 }}
animate={inView ? { scaleX: 1 } : {}}
transition={{ duration: 0.9, delay: 0.5, ease: EASE }}
style={{
height: "1px",
background: "var(--color-border)",
transformOrigin: "left",
}}
/>
</div>
{/* Bottom bar */}
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: 0.65, ease: EASE }}
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
flexWrap: "wrap",
gap: "1rem",
}}
>
<span
style={{
fontSize: "0.8125rem",
color: "var(--color-foreground-muted)",
}}
>
{copyright}
</span>
{socialLinks.length > 0 && (
<div style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>
{socialLinks.map((s) => (
<SocialPill key={s.href} {...s} />
))}
</div>
)}
</motion.div>
</div>
</footer>
);
}