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
| Prop | Type | Default | Description |
|---|---|---|---|
| brand | ReactNode | Wordmark or logo, usually a link to the home page. | |
| description | string | One 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. |
| bottom | ReactNode | Right side of the bottom bar: small links or social icons. | |
| legal | string | Left 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