Skip to content
goodtabs UI
BrowseBrowse components

Footer columns

A site footer with a brand line, columns of links, a hairline and a legal bar.

npx shadcn@latest add https://ui.goodtabs.co/r/footer-columns.json

With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/footer-columns

Brand and one line, four columns, a legal line and two small links.

Minimal

No columns: one line and three links in the bottom bar.

With social icons

Three columns and icon links in the bottom bar, inline SVGs in currentColor.

Usage

import { FooterColumns } from "@/components/ui/footer-columns"

<FooterColumns
  brand={<Link href="/">Acme</Link>}
  description="Billing for agencies."
  columns={[
    { title: "Product", links: [{ label: "Pricing", href: "/pricing" }] },
    { title: "Company", links: [{ label: "About", href: "/about" }] },
  ]}
  legal="2026 Acme Ltd"
/>

Props

PropTypeDefaultDescription
brandReactNodeWordmark or logo, usually a link to the home page.
descriptionstringOne line under the brand.
columns{ title: string; links: { label: string; href: string; external?: boolean }[] }[][]Columns of links with a mono uppercase heading each. Three or four reads best.
bottomReactNodeRight side of the bottom bar: small links or social icons.
legalstringLeft side of the bottom bar, usually the copyright line.

Source

footer-columns/footer-columns.tsx
import * as React from "react";
import Link from "next/link";
import { ArrowUpRightIcon } from "lucide-react";
import { cn } from "@/lib/utils";

export type FooterLink = {
  label: string;
  href: string;
  /** Opens in a new tab with a small arrow after the label. */
  external?: boolean;
};

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

type FooterColumnsProps = Omit<React.ComponentProps<"footer">, "children"> & {
  /** Wordmark or logo, usually a link to the home page. */
  brand: React.ReactNode;
  /** One line under the brand. */
  description?: string;
  /** Three or four columns of links, each with a mono heading. */
  columns?: FooterColumn[];
  /** Right side of the bottom bar: small links or social icons. */
  bottom?: React.ReactNode;
  /** Left side of the bottom bar, usually the copyright line. */
  legal?: string;
};

/**
 * A site footer: brand and one line on the left, columns of links on the right,
 * then a hairline and a bottom bar with the legal line and small links.
 */
export function FooterColumns({
  brand,
  description,
  columns = [],
  bottom,
  legal,
  className,
  ...props
}: FooterColumnsProps) {
  return (
    <footer data-slot="footer-columns" className={cn("w-full text-sm", className)} {...props}>
      <div className="mx-auto flex max-w-6xl flex-col gap-10 px-6 py-10 md:flex-row md:justify-between md:px-8">
        <div className="flex max-w-xs flex-col gap-3">
          <div className="flex items-center text-base font-medium">{brand}</div>
          {description && <p className="m-0 text-muted-foreground">{description}</p>}
        </div>
        {columns.length > 0 && (
          <nav
            aria-label="Footer"
            className="grid grid-cols-2 gap-x-8 gap-y-8 sm:grid-cols-[repeat(var(--columns),minmax(0,1fr))] sm:gap-x-12"
            style={{ "--columns": columns.length } as React.CSSProperties}
          >
            {columns.map((column) => (
              <div key={column.title} className="flex flex-col gap-3">
                <p className="m-0 font-mono text-xs tracking-wide text-muted-foreground uppercase">{column.title}</p>
                <ul className="m-0 flex list-none flex-col gap-2 p-0">
                  {column.links.map((link) => (
                    <li key={`${link.href}-${link.label}`}>
                      <FooterAnchor link={link} />
                    </li>
                  ))}
                </ul>
              </div>
            ))}
          </nav>
        )}
      </div>
      {(legal || bottom) && (
        <div className="border-t">
          <div className="mx-auto flex max-w-6xl flex-col gap-3 px-6 py-5 text-xs text-muted-foreground sm:flex-row sm:items-center sm:justify-between md:px-8">
            {legal && <p className="m-0">{legal}</p>}
            {bottom && <div className="flex flex-wrap items-center gap-4">{bottom}</div>}
          </div>
        </div>
      )}
    </footer>
  );
}

function FooterAnchor({ link }: { link: FooterLink }) {
  const classes =
    "inline-flex items-center gap-1 rounded-sm text-muted-foreground transition-colors duration-100 ease-out hover:text-foreground";
  if (link.external) {
    return (
      <a href={link.href} target="_blank" rel="noreferrer" className={classes}>
        {link.label}
        <ArrowUpRightIcon className="size-3" aria-hidden="true" />
        <span className="sr-only">, opens in a new tab</span>
      </a>
    );
  }
  return (
    <Link href={link.href} className={classes}>
      {link.label}
    </Link>
  );
}

For AI agents

Hand your agent this page, the markdown at /components/footer-columns/prompt.md, or the Copy AI prompt button above.

When to use

  • The bottom of a marketing site or docs site with eight or more secondary links
  • When legal links and social profiles need a home that is not the header
  • Without columns, as a one-line footer for a newsletter or a small app

When not to use

  • Inside an app shell where a sidebar already holds the secondary links
  • On a landing page with a single call to action: a one-line footer is enough
  • For more than five columns: group the links or move some into docs

Agent notes

  • Server component. Internal links use next/link; set external to open in a new tab with a small arrow
  • The column count follows columns.length from sm up and is two columns below it
  • The bottom bar only renders when legal or bottom is set; put small links or social icons in bottom
  • It sets no background: place it on bg-background and the hairlines do the rest