SaaS footer
Product and resource columns, a legal row, and a live system-status link.
Live preview
Source
This exact file renders the preview above.
import Link from 'next/link'
import { BrandMark } from '@/components/library/brand'
import { Container } from '@/components/ui/layout'
import { Status } from '@/components/ui/status'
import { productLinks, resourceLinks } from '@/content/nav-demo'
/**
* SaaS footer
*
* Product and resources columns, a compliance row, and — the piece SaaS
* footers most often omit — a link to system status with its live state.
* Trust signals belong where people look for them when something is wrong.
*/
const legal = [
{ label: 'Privacy', href: '/about' },
{ label: 'Terms', href: '/about' },
{ label: 'Security', href: '/docs/installation' },
{ label: 'Sub-processors', href: '/about' },
]
export default function SaasFooter() {
return (
<footer className="border-t border-line bg-canvas">
<Container className="py-12">
<div className="grid gap-10 lg:grid-cols-[1.4fr_1fr_1fr]">
<div className="max-w-sm">
<div className="flex items-center gap-2 text-ink-strong">
<BrandMark className="size-5 text-accent" />
<span className="display-type text-md font-semibold">Foundry</span>
</div>
<p className="mt-3 text-sm text-ink-muted">
The complete frontend building-block library for teams that would rather compose than
rebuild.
</p>
<Link
href="/changelog"
className="mt-5 inline-flex items-center gap-2 rounded-md border border-line bg-surface px-3 py-2 transition-colors hover:border-line-strong"
>
<Status appearance="dot" kind="operational" label="All systems operational" />
</Link>
</div>
<nav aria-label="Product">
<h2 className="label-caps mb-3 text-ink-subtle">Product</h2>
<ul className="flex flex-col gap-2">
{productLinks.map((link) => (
<li key={link.href}>
<Link
href={link.href}
className="text-sm text-ink-muted transition-colors hover:text-accent"
>
{link.label}
</Link>
</li>
))}
</ul>
</nav>
<nav aria-label="Resources">
<h2 className="label-caps mb-3 text-ink-subtle">Resources</h2>
<ul className="flex flex-col gap-2">
{resourceLinks.map((link) => (
<li key={link.href}>
<Link
href={link.href}
className="text-sm text-ink-muted transition-colors hover:text-accent"
>
{link.label}
</Link>
</li>
))}
</ul>
</nav>
</div>
<div className="mt-10 flex flex-col gap-3 border-t border-line pt-6 sm:flex-row sm:items-center sm:justify-between">
<nav aria-label="Legal">
<ul className="flex flex-wrap gap-x-5 gap-y-1">
{legal.map((link) => (
<li key={link.label}>
<Link
href={link.href}
className="text-xs text-ink-subtle transition-colors hover:text-accent"
>
{link.label}
</Link>
</li>
))}
</ul>
</nav>
<p className="text-xs text-ink-subtle">Demo build — no data is collected.</p>
</div>
</Container>
</footer>
)
}
components/blocks/footers/saas.tsx
import Link from 'next/link'
import { BrandMark } from '@/components/library/brand'
import { Container } from '@/components/ui/layout'
import { Status } from '@/components/ui/status'
import { productLinks, resourceLinks } from '@/content/nav-demo'
/**
* SaaS footer
*
* Product and resources columns, a compliance row, and — the piece SaaS
* footers most often omit — a link to system status with its live state.
* Trust signals belong where people look for them when something is wrong.
*/
const legal = [
{ label: 'Privacy', href: '/about' },
{ label: 'Terms', href: '/about' },
{ label: 'Security', href: '/docs/installation' },
{ label: 'Sub-processors', href: '/about' },
]
export default function SaasFooter() {
return (
<footer className="border-t border-line bg-canvas">
<Container className="py-12">
<div className="grid gap-10 lg:grid-cols-[1.4fr_1fr_1fr]">
<div className="max-w-sm">
<div className="flex items-center gap-2 text-ink-strong">
<BrandMark className="size-5 text-accent" />
<span className="display-type text-md font-semibold">Foundry</span>
</div>
<p className="mt-3 text-sm text-ink-muted">
The complete frontend building-block library for teams that would rather compose than
rebuild.
</p>
<Link
href="/changelog"
className="mt-5 inline-flex items-center gap-2 rounded-md border border-line bg-surface px-3 py-2 transition-colors hover:border-line-strong"
>
<Status appearance="dot" kind="operational" label="All systems operational" />
</Link>
</div>
<nav aria-label="Product">
<h2 className="label-caps mb-3 text-ink-subtle">Product</h2>
<ul className="flex flex-col gap-2">
{productLinks.map((link) => (
<li key={link.href}>
<Link
href={link.href}
className="text-sm text-ink-muted transition-colors hover:text-accent"
>
{link.label}
</Link>
</li>
))}
</ul>
</nav>
<nav aria-label="Resources">
<h2 className="label-caps mb-3 text-ink-subtle">Resources</h2>
<ul className="flex flex-col gap-2">
{resourceLinks.map((link) => (
<li key={link.href}>
<Link
href={link.href}
className="text-sm text-ink-muted transition-colors hover:text-accent"
>
{link.label}
</Link>
</li>
))}
</ul>
</nav>
</div>
<div className="mt-10 flex flex-col gap-3 border-t border-line pt-6 sm:flex-row sm:items-center sm:justify-between">
<nav aria-label="Legal">
<ul className="flex flex-wrap gap-x-5 gap-y-1">
{legal.map((link) => (
<li key={link.label}>
<Link
href={link.href}
className="text-xs text-ink-subtle transition-colors hover:text-accent"
>
{link.label}
</Link>
</li>
))}
</ul>
</nav>
<p className="text-xs text-ink-subtle">Demo build — no data is collected.</p>
</div>
</Container>
</footer>
)
}
components/ui/status.tsx
import { CheckCircle2, AlertTriangle, XCircle, Info, CircleDashed, Clock } from 'lucide-react'
import { cn } from '@/lib/cn'
/**
* Status
*
* Deliberately separate from Badge. A status describes the *state of a thing*
* (a deployment, an invoice, a service) and therefore always pairs an icon
* with a label — colour is the third signal, never the only one. That rule is
* what makes the library legible to colour-blind users without a theme switch.
*/
export type StatusKind = 'operational' | 'degraded' | 'down' | 'pending' | 'info' | 'idle'
export interface StatusProps {
kind: StatusKind
label: string
/** `dot` for dense tables, `pill` for standalone display. */
appearance?: 'dot' | 'pill' | 'inline'
className?: string
}
const config = {
operational: {
icon: CheckCircle2,
text: 'text-success',
bg: 'bg-success-soft border-success-line',
dot: 'bg-success',
},
degraded: {
icon: AlertTriangle,
text: 'text-warning',
bg: 'bg-warning-soft border-warning-line',
dot: 'bg-warning',
},
down: {
icon: XCircle,
text: 'text-danger',
bg: 'bg-danger-soft border-danger-line',
dot: 'bg-danger',
},
pending: { icon: Clock, text: 'text-info', bg: 'bg-info-soft border-info-line', dot: 'bg-info' },
info: { icon: Info, text: 'text-info', bg: 'bg-info-soft border-info-line', dot: 'bg-info' },
idle: {
icon: CircleDashed,
text: 'text-ink-subtle',
bg: 'bg-surface-sunken border-line',
dot: 'bg-ink-subtle',
},
} as const
export function Status({ kind, label, appearance = 'inline', className }: StatusProps) {
const entry = config[kind]
const Icon = entry.icon
if (appearance === 'dot') {
return (
<span className={cn('inline-flex items-center gap-2 text-sm text-ink', className)}>
<span className={cn('size-2 shrink-0 rounded-full', entry.dot)} aria-hidden="true" />
{label}
</span>
)
}
if (appearance === 'pill') {
return (
<span
className={cn(
'inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-medium',
entry.bg,
entry.text,
className,
)}
>
<Icon className="size-3.5 shrink-0" aria-hidden="true" />
{label}
</span>
)
}
return (
<span
className={cn('inline-flex items-center gap-1.5 text-sm font-medium', entry.text, className)}
>
<Icon className="size-4 shrink-0" aria-hidden="true" />
{label}
</span>
)
}
Demo source — adapt to your project. Foundry is not published as a package.
Usage
Trust signals belong where people look for them when something is wrong. The status link is the piece SaaS footers most often omit.
- Keep legal links small but present; hiding them entirely reads as evasive.
- The status card is a link, not a decoration — it should go somewhere real.
Variants and states
Every entry below is a genuine difference in behaviour or layout, and every one of them is visible in the preview above.
- Three-column at lg
- Status link card
- Legal row
Accessibility
- Status link
- The whole card is one focus stop, with the state announced as text.
- Named columns
- Both link groups are labelled navigation landmarks.
Foundry implements published ARIA patterns and is tested against them. No WCAG certification is claimed — see the accessibility documentation for what is and is not covered.
Related
All navigationSaaS starter
A twelve-route subscription product: marketing site, authentication, an in-product dashboard and account management, all from the same component set.
advancedFeatured4 variantsSaaS navbar
Product dropdown, flat secondary links, and a separated sign-in / sign-up pair.
intermediateFeatured4 variantsMulti-column footer
Brand statement plus three labelled navigation groups, each its own landmark.
starterFeatured3 variants