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>
  );
}

Avis

Footer Animated Links — React Footer Section — Incubator