Skip to content

SaaS footer

Product and resource columns, a legal row, and a live system-status link.

Footersstarterfootersaasstatuslegaltrust

Live preview

full widthLive preview — open it in a new tab for the full-height version.
Open the preview in a new tab

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.