# Footer

> Site footer with a brand blurb, four link columns, a newsletter field that swaps to an inline success state, and a base row with legal links and a pulsing status dot.

- Collection: Blocks
- Tier: free
- Docs: https://geturui.com/docs/footer-block
- Install: `npx shadcn@latest add @geturui/footer-block`
- Dependencies: motion, lucide-react

## Usage

```tsx
import { FooterBlock } from "@/components/blocks/footer-block";

export default function SiteFooter() {
  return (
    <FooterBlock
      brand={{ name: "Fieldnote", description: "Research notes your whole team can search." }}
      columns={[
        { title: "Product", links: [{ label: "Features", href: "/features" }, { label: "Pricing", href: "/pricing" }] },
        { title: "Company", links: [{ label: "About", href: "/about" }, { label: "Careers", href: "/careers", badge: "Hiring" }] },
      ]}
      newsletter={{
        title: "Product updates",
        onSubscribe: (email) => fetch("/api/subscribe", { method: "POST", body: email }).then(() => undefined),
      }}
      copyright="© 2026 Fieldnote Inc."
    />
  );
}
```

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| brand | `{ name: string; description?: string; href?: string }` | — | Logo tile, name and blurb. |
| columns | `{ title: string; links: { label; href; badge? }[] }[]` | — | Link columns; four fit best. |
| newsletter | `{ title; description?; placeholder?; onSubscribe? } | null` | — | Email signup. Resolve onSubscribe to show the success state. null hides it. |
| copyright | `string` | — | Base-row text. |
| legal | `FooterLink[]` | — | Base-row links. |
| status | `string | null` | "All systems normal" | Status line with a pulsing dot. null hides it. |
| socials | `{ label; href; icon }[]` | — | Icon links; label becomes the aria-label. |

## Accessibility

- Links sit in a nav labelled Footer, grouped under real headings.
- The newsletter form is labelled by its heading, validates the email with an inline error, and announces success in a status region.
- Icon-only social links carry aria-labels.

## Source

### components/blocks/footer-block.tsx

```tsx
"use client";

import * as React from "react";
import { AnimatePresence, motion } from "motion/react";
import { ArrowRight, Check, Mail, Rss, Sparkles } from "lucide-react";
import { cn } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";

export interface FooterLink {
  label: string;
  href: string;
  /** Small label after the link, e.g. "New". */
  badge?: string;
}

export interface FooterColumn {
  title: string;
  links: FooterLink[];
}

export interface FooterNewsletter {
  title: string;
  description?: string;
  placeholder?: string;
  /** Resolve to show the subscribed state; reject to show an error. */
  onSubscribe?: (email: string) => Promise<void> | void;
}

export interface FooterBlockProps {
  brand?: { name: string; description?: string; href?: string };
  /** Up to four columns read best on desktop. */
  columns?: FooterColumn[];
  /** Pass null to hide the newsletter form. */
  newsletter?: FooterNewsletter | null;
  /** Text on the base row, e.g. "© 2026 GetUrUI". */
  copyright?: string;
  legal?: FooterLink[];
  /** Status line with a pulsing dot. Pass null to hide. */
  status?: string | null;
  socials?: { label: string; href: string; icon: React.ReactNode }[];
  className?: string;
}

const DEFAULT_COLUMNS: FooterColumn[] = [
  {
    title: "Product",
    links: [
      { label: "Components", href: "#" },
      { label: "Blocks", href: "#", badge: "New" },
      { label: "Templates", href: "#" },
      { label: "Pricing", href: "#" },
    ],
  },
  {
    title: "Developers",
    links: [
      { label: "Documentation", href: "#" },
      { label: "CLI", href: "#" },
      { label: "MCP server", href: "#" },
      { label: "Changelog", href: "#" },
    ],
  },
  {
    title: "Resources",
    links: [
      { label: "Motion guide", href: "#" },
      { label: "Design tokens", href: "#" },
      { label: "Showcase", href: "#" },
      { label: "Licence", href: "#" },
    ],
  },
  {
    title: "Company",
    links: [
      { label: "About", href: "#" },
      { label: "Blog", href: "#" },
      { label: "Careers", href: "#", badge: "Hiring" },
      { label: "Contact", href: "#" },
    ],
  },
];

const DEFAULT_NEWSLETTER: FooterNewsletter = {
  title: "Get new components first",
  description: "One short email when something ships. No spam, unsubscribe any time.",
  placeholder: "you@company.com",
};

const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;

function Newsletter({ newsletter }: { newsletter: FooterNewsletter }) {
  const [email, setEmail] = React.useState("");
  const [state, setState] = React.useState<"idle" | "loading" | "done">("idle");
  const [error, setError] = React.useState<string | undefined>();
  const id = React.useId();

  async function submit(e: React.FormEvent) {
    e.preventDefault();
    if (state !== "idle") return;
    if (!EMAIL_RE.test(email.trim())) {
      setError("Enter a valid email address");
      return;
    }
    setError(undefined);
    setState("loading");
    try {
      if (newsletter.onSubscribe) await newsletter.onSubscribe(email.trim());
      else await new Promise((r) => setTimeout(r, 700));
      setState("done");
    } catch {
      setState("idle");
      setError("Something went wrong. Try again.");
    }
  }

  return (
    <div className="grid gap-3">
      <div className="grid gap-1">
        <h3 id={`${id}-title`} className="text-sm font-semibold text-ink">
          {newsletter.title}
        </h3>
        {newsletter.description && <p className="text-[13px] leading-5 text-ink-muted">{newsletter.description}</p>}
      </div>
      <AnimatePresence mode="popLayout" initial={false}>
        {state === "done" ? (
          <motion.p
            key="done"
            role="status"
            initial={{ opacity: 0, y: 6, filter: "blur(8px)" }}
            animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
            transition={{ type: "spring", stiffness: 320, damping: 28 }}
            className="inline-flex h-10 items-center gap-2 rounded-[10px] bg-volt-soft px-3 text-sm font-medium text-volt-ink"
          >
            <Check className="size-4" aria-hidden /> You are on the list
          </motion.p>
        ) : (
          <motion.form
            key="form"
            noValidate
            onSubmit={submit}
            aria-labelledby={`${id}-title`}
            exit={{ opacity: 0, y: -6, filter: "blur(8px)" }}
            transition={{ duration: 0.2 }}
          >
            <Input
              type="email"
              name="email"
              aria-label="Email address"
              placeholder={newsletter.placeholder ?? "you@company.com"}
              autoComplete="email"
              icon={<Mail className="size-4" aria-hidden />}
              value={email}
              error={error}
              onChange={(e) => {
                setEmail(e.target.value);
                if (error) setError(undefined);
              }}
              className="pr-1"
              trailing={
                <motion.button
                  type="submit"
                  whileTap={{ scale: 0.92 }}
                  aria-label="Subscribe"
                  aria-busy={state === "loading" || undefined}
                  className="grid size-8 shrink-0 place-items-center rounded-[8px] bg-volt text-on-volt outline-none transition-shadow hover:shadow-glow focus-visible:shadow-[var(--focus-ring)]"
                >
                  <motion.span animate={state === "loading" ? { x: [0, 4, 0] } : { x: 0 }} transition={{ duration: 0.6, repeat: state === "loading" ? Infinity : 0 }}>
                    <ArrowRight className="size-4" />
                  </motion.span>
                </motion.button>
              }
            />
          </motion.form>
        )}
      </AnimatePresence>
    </div>
  );
}

/** Site footer: brand blurb, four link columns, a newsletter field with an inline success state, and a base row with legal links and a live status dot. */
export function FooterBlock({
  brand = { name: "GetUrUI", description: "Animated React components and page blocks you copy into your app and make your own." },
  columns = DEFAULT_COLUMNS,
  newsletter = DEFAULT_NEWSLETTER,
  copyright = "© 2026 GetUrUI. All rights reserved.",
  legal = [
    { label: "Privacy", href: "#" },
    { label: "Terms", href: "#" },
    { label: "Cookies", href: "#" },
  ],
  status = "All systems normal",
  socials = [
    { label: "RSS feed", href: "#", icon: <Rss className="size-4" /> },
    { label: "Email us", href: "#", icon: <Mail className="size-4" /> },
    { label: "What's new", href: "#", icon: <Sparkles className="size-4" /> },
  ],
  className,
}: FooterBlockProps) {
  return (
    <footer className={cn("@container w-full border-t border-border bg-bg px-4 pt-14 sm:px-6 sm:pt-16", className)}>
      <div className="mx-auto grid max-w-6xl gap-12">
        <div className="grid gap-12 @4xl:grid-cols-[minmax(0,1.3fr)_minmax(0,2.7fr)] @4xl:gap-16">
          <div className="grid content-start gap-6">
            <a href={brand.href ?? "#"} className="inline-flex items-center gap-2.5 justify-self-start rounded-md outline-none focus-visible:shadow-[var(--focus-ring)]">
              <span aria-hidden className="grid size-8 place-items-center rounded-[10px] bg-volt font-display text-[15px] font-bold text-on-volt">
                {brand.name.charAt(0)}
              </span>
              <span className="font-display text-lg font-semibold tracking-[-0.3px] text-ink">{brand.name}</span>
            </a>
            {brand.description && <p className="max-w-xs text-sm leading-[22px] text-ink-muted">{brand.description}</p>}
            {newsletter && (
              <div className="max-w-sm">
                <Newsletter newsletter={newsletter} />
              </div>
            )}
          </div>

          <nav aria-label="Footer" className="grid grid-cols-2 gap-x-6 gap-y-10 @xl:grid-cols-4">
            {columns.map((col) => (
              <div key={col.title} className="grid content-start gap-4">
                <h3 className="text-[13px] font-semibold text-ink">{col.title}</h3>
                <ul className="grid gap-2.5">
                  {col.links.map((l) => (
                    <li key={l.label}>
                      <a
                        href={l.href}
                        className="group/link inline-flex items-center gap-2 rounded-xs text-sm text-ink-muted outline-none transition-colors hover:text-ink focus-visible:shadow-[var(--focus-ring)]"
                      >
                        <span className="transition-transform duration-300 ease-[var(--ease-spring)] group-hover/link:translate-x-0.5">{l.label}</span>
                        {l.badge && <Badge tone="new">{l.badge}</Badge>}
                      </a>
                    </li>
                  ))}
                </ul>
              </div>
            ))}
          </nav>
        </div>

        <div className="flex flex-col gap-4 border-t border-border py-6 text-[13px] text-ink-subtle @xl:flex-row @xl:items-center @xl:justify-between">
          <div className="flex flex-wrap items-center gap-x-5 gap-y-2">
            <span>{copyright}</span>
            {legal.map((l) => (
              <a key={l.label} href={l.href} className="rounded-xs outline-none transition-colors hover:text-ink focus-visible:shadow-[var(--focus-ring)]">
                {l.label}
              </a>
            ))}
          </div>
          <div className="flex items-center gap-4">
            {status && (
              <span className="inline-flex items-center gap-2 text-ink-muted">
                <span aria-hidden className="size-1.5 rounded-full bg-success text-success animate-pulse-dot" />
                {status}
              </span>
            )}
            {socials.length > 0 && (
              <div className="flex items-center gap-1">
                {socials.map((s) => (
                  <a
                    key={s.label}
                    href={s.href}
                    aria-label={s.label}
                    className="grid size-8 place-items-center rounded-full text-ink-subtle outline-none transition-colors hover:bg-surface-hover hover:text-ink focus-visible:shadow-[var(--focus-ring)]"
                  >
                    {s.icon}
                  </a>
                ))}
              </div>
            )}
          </div>
        </div>
      </div>
    </footer>
  );
}

```
