deepmoon

Footer Variants

Three layouts sharing the same link-group data shape.

Minimal

Links + newsletter

Mega

Installation

$ npx shadcn@latest add https://deepmoon.dev/r/footer-variants.json

Props

FooterMinimalProps
PropTypeRequired
logoReact.ReactNodeNo
copyrightstringYes
links{ label: string; href: string }[]No
FooterLinksProps
PropTypeRequired
logoReact.ReactNodeNo
groupsFooterLinkGroup[]Yes
copyrightstringYes
onSubscribe(email: string) => voidNo
FooterMegaProps
PropTypeRequired
logoReact.ReactNodeNo
descriptionstringNo
groupsFooterLinkGroup[]Yes
bottomLinks{ label: string; href: string }[]No
copyrightstringYes

Code

footer-variants.tsx
"use client";

import * as React from "react";

import { cn } from "@/lib/utils";

export interface FooterLinkGroup {
  title: string;
  links: { label: string; href: string }[];
}

export interface FooterMinimalProps extends React.ComponentPropsWithoutRef<"footer"> {
  logo?: React.ReactNode;
  copyright: string;
  links?: { label: string; href: string }[];
}

export function FooterMinimal({ logo, copyright, links = [], className, ...props }: FooterMinimalProps) {
  return (
    <footer
      className={cn(
        "flex flex-col items-center justify-between gap-4 border-t border-border py-6 sm:flex-row",
        className,
      )}
      {...props}
    >
      <div className="flex items-center gap-3 text-sm text-muted-foreground">
        {logo}
        <span>{copyright}</span>
      </div>
      {links.length > 0 ? (
        <nav aria-label="Footer" className="flex items-center gap-4 text-sm">
          {links.map((link) => (
            <a key={link.href} href={link.href} className="text-muted-foreground hover:text-foreground">
              {link.label}
            </a>
          ))}
        </nav>
      ) : null}
    </footer>
  );
}

export interface FooterLinksProps extends React.ComponentPropsWithoutRef<"footer"> {
  logo?: React.ReactNode;
  groups: FooterLinkGroup[];
  copyright: string;
  onSubscribe?: (email: string) => void;
}

export function FooterLinks({ logo, groups, copyright, onSubscribe, className, ...props }: FooterLinksProps) {
  const [email, setEmail] = React.useState("");

  return (
    <footer className={cn("border-t border-border py-12", className)} {...props}>
      <div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-4">
        <div className="flex flex-col gap-3 lg:col-span-1">
          {logo}
          <p className="text-sm text-muted-foreground">Stay up to date with product news.</p>
          {onSubscribe ? (
            <form
              className="flex gap-2"
              onSubmit={(event) => {
                event.preventDefault();
                if (email.trim()) {
                  onSubscribe(email.trim());
                  setEmail("");
                }
              }}
            >
              <label htmlFor="footer-newsletter-email" className="sr-only">
                Email address
              </label>
              <input
                id="footer-newsletter-email"
                type="email"
                required
                value={email}
                onChange={(event) => setEmail(event.target.value)}
                placeholder="you@example.com"
                className="w-full min-w-0 rounded-md border border-input bg-background px-3 py-1.5 text-sm text-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring"
              />
              <button
                type="submit"
                className="shrink-0 rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground"
              >
                Subscribe
              </button>
            </form>
          ) : null}
        </div>
        {groups.map((group) => (
          <div key={group.title} className="flex flex-col gap-3">
            <p className="text-sm font-semibold text-foreground">{group.title}</p>
            <ul role="list" className="flex list-none flex-col gap-2">
              {group.links.map((link) => (
                <li key={link.href}>
                  <a href={link.href} className="text-sm text-muted-foreground hover:text-foreground">
                    {link.label}
                  </a>
                </li>
              ))}
            </ul>
          </div>
        ))}
      </div>
      <p className="mt-10 text-sm text-muted-foreground">{copyright}</p>
    </footer>
  );
}

export interface FooterMegaProps extends React.ComponentPropsWithoutRef<"footer"> {
  logo?: React.ReactNode;
  description?: string;
  groups: FooterLinkGroup[];
  bottomLinks?: { label: string; href: string }[];
  copyright: string;
}

export function FooterMega({
  logo,
  description,
  groups,
  bottomLinks = [],
  copyright,
  className,
  ...props
}: FooterMegaProps) {
  return (
    <footer className={cn("border-t border-border bg-secondary/30 py-16", className)} {...props}>
      <div className="grid grid-cols-1 gap-10 sm:grid-cols-3 lg:grid-cols-5">
        <div className="flex flex-col gap-3 sm:col-span-3 lg:col-span-2">
          {logo}
          {description ? <p className="max-w-sm text-sm text-muted-foreground">{description}</p> : null}
        </div>
        {groups.map((group) => (
          <div key={group.title} className="flex flex-col gap-3">
            <p className="text-sm font-semibold text-foreground">{group.title}</p>
            <ul role="list" className="flex list-none flex-col gap-2">
              {group.links.map((link) => (
                <li key={link.href}>
                  <a href={link.href} className="text-sm text-muted-foreground hover:text-foreground">
                    {link.label}
                  </a>
                </li>
              ))}
            </ul>
          </div>
        ))}
      </div>
      <div className="mt-12 flex flex-col items-center justify-between gap-4 border-t border-border pt-6 sm:flex-row">
        <p className="text-sm text-muted-foreground">{copyright}</p>
        {bottomLinks.length > 0 ? (
          <nav aria-label="Legal" className="flex items-center gap-4 text-sm">
            {bottomLinks.map((link) => (
              <a key={link.href} href={link.href} className="text-muted-foreground hover:text-foreground">
                {link.label}
              </a>
            ))}
          </nav>
        ) : null}
      </div>
    </footer>
  );
}