Retour au catalogue
Footer Animated Links
Footer avec liens a underline anime gauche-droite, icones sociales bouncy, et entree stagger des colonnes. Hovers premium.
footermedium Both Responsive a11y
elegantminimaleditorialagencysaasportfoliouniversalstacked
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 FooterAnimatedLinksProps {
brandName?: string;
brandDescription?: string;
columns?: FooterColumn[];
socialLinks?: SocialLink[];
copyright?: string;
}
const EASE = [0.16, 1, 0.3, 1] as const;
function AnimatedLink({ label, href }: { label: string; href: string }) {
return (
<a
href={href}
style={{
position: "relative",
fontSize: "0.875rem",
color: "var(--color-foreground-muted)",
textDecoration: "none",
display: "inline-block",
paddingBottom: "2px",
transition: "color var(--duration-fast) var(--ease-out)",
}}
>
{label}
<span
style={{
position: "absolute",
bottom: 0,
left: 0,
height: "1px",
width: "0%",
background: "var(--color-accent)",
transition: "width 0.35s cubic-bezier(0.16,1,0.3,1)",
}}
className="group-hover:!w-full"
data-underline
/>
<style>{`
a:hover [data-underline] { width: 100% !important; }
a:hover { color: var(--color-foreground) !important; }
`}</style>
</a>
);
}
function SocialIcon({ label, href }: SocialLink) {
return (
<motion.a
href={href}
whileHover={{ scale: 1.2 }}
whileTap={{ scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 15 }}
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "40px",
height: "40px",
borderRadius: "var(--radius-full)",
border: "1px solid var(--color-border)",
color: "var(--color-foreground-muted)",
textDecoration: "none",
fontSize: "0.75rem",
fontWeight: 600,
letterSpacing: "0.02em",
transition: "border-color var(--duration-fast), color var(--duration-fast)",
}}
>
{label.charAt(0).toUpperCase()}
</motion.a>
);
}
export default function FooterAnimatedLinks({
brandName = "Brand",
brandDescription = "",
columns = [],
socialLinks = [],
copyright = "2026 Brand. Tous droits reserves.",
}: FooterAnimatedLinksProps) {
const ref = useRef<HTMLElement>(null);
const inView = useInView(ref, { once: true, margin: "-60px" });
return (
<footer
ref={ref}
style={{
background: "var(--color-background-alt, var(--color-background))",
padding: "var(--section-padding-y) 0 2rem",
}}
>
<div
style={{
maxWidth: "var(--container-max-width)",
margin: "0 auto",
padding: "0 var(--container-padding-x)",
}}
>
<div
style={{
display: "grid",
gridTemplateColumns: "1.5fr repeat(auto-fit, minmax(140px, 1fr))",
gap: "3rem",
marginBottom: "3rem",
}}
>
{/* Brand column */}
<motion.div
initial={{ opacity: 0, y: 24 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, ease: EASE }}
>
<p
style={{
fontFamily: "var(--font-serif, var(--font-sans))",
fontSize: "1.5rem",
fontWeight: 700,
color: "var(--color-foreground)",
marginBottom: "0.75rem",
}}
>
{brandName}
</p>
{brandDescription && (
<p style={{ fontSize: "0.875rem", lineHeight: 1.6, color: "var(--color-foreground-muted)", maxWidth: "280px" }}>
{brandDescription}
</p>
)}
{socialLinks.length > 0 && (
<div style={{ display: "flex", gap: "0.5rem", marginTop: "1.5rem" }}>
{socialLinks.map((s) => (
<SocialIcon key={s.href} {...s} />
))}
</div>
)}
</motion.div>
{/* Link columns with stagger */}
{columns.map((col, i) => (
<motion.div
key={col.title}
initial={{ opacity: 0, y: 24 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.1 * (i + 1), ease: EASE }}
>
<p
style={{
fontSize: "0.75rem",
fontWeight: 600,
textTransform: "uppercase",
letterSpacing: "0.08em",
color: "var(--color-foreground)",
marginBottom: "1rem",
}}
>
{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}>
<AnimatedLink {...link} />
</li>
))}
</ul>
</motion.div>
))}
</div>
{/* Bottom bar */}
<motion.div
initial={{ opacity: 0 }}
animate={inView ? { opacity: 1 } : {}}
transition={{ duration: 0.5, delay: 0.4, ease: EASE }}
style={{
borderTop: "1px solid var(--color-border)",
paddingTop: "1.5rem",
textAlign: "center",
}}
>
<span style={{ fontSize: "0.8125rem", color: "var(--color-foreground-muted)" }}>
{copyright}
</span>
</motion.div>
</div>
</footer>
);
}