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

Avis

Footer Animated Reveal — React Footer Section — Incubator