Footer
The last band: the sitemap in columns, the legal links and the fine print.
One-row footer
footer-01The mark, six pages and the copyright on a single line on a wide screen, stacked on a phone.
import * as React from "react"
import Link from "next/link"
import { LogoMark } from "@/components/ui/logo-mark"
import { BRAND, COPYRIGHT, LINKS, NAV_LABEL } from "./footer-01.data"
/**
* The smallest whole footer: the mark, the six pages a visitor looks for last
* and the copyright, on one line from `lg` and stacked below it. For a
* product page or a launch site that has no sitemap to walk.
*
* A server component with no state; every word is in `./footer-01.data`.
*/
export function Footer01() {
return (
<footer
data-section="footer-01"
data-slot="section"
className="w-full border-t border-rule bg-background px-4 py-8 text-foreground md:px-6"
>
<div className="mx-auto flex max-w-7xl flex-col gap-6 lg:flex-row lg:items-center lg:justify-between lg:gap-10">
<Link href={BRAND.href} aria-label={BRAND.homeLabel} className="flex w-fit rounded-md focus-ring">
<LogoMark name="Northwind" />
</Link>
<nav aria-label={NAV_LABEL}>
<ul className="flex flex-wrap gap-x-6 gap-y-2 text-sm/6">
{LINKS.map((link) => (
<li key={link.href}>
<Link
href={link.href}
className="rounded-sm text-muted-foreground transition-colors duration-(--duration-fast) ease-(--ease-standard) focus-ring hover:text-foreground"
>
{link.label}
</Link>
</li>
))}
</ul>
</nav>
<p className="text-sm/6 text-muted-foreground">
© {COPYRIGHT.year} {COPYRIGHT.holder}
</p>
</div>
</footer>
)
}
export default Footer01/**
* Every word `footer-01` sets: where the mark leads, the pages the one row
* links and the copyright line. The links name routes of your own site, so
* replacing this file is the whole edit.
*/
export type FooterLink = { label: string; href: string }
/** The mark links home; `homeLabel` is what a screen reader hears for it. */
export const BRAND = { href: "/", homeLabel: "Northwind home" } as const
/** Names the row's one nav, so a reader who lists landmarks hears what it holds. */
export const NAV_LABEL = "Site"
export const LINKS: FooterLink[] = [
{ label: "Product", href: "/product" },
{ label: "Pricing", href: "/pricing" },
{ label: "Customers", href: "/customers" },
{ label: "Changelog", href: "/changelog" },
{ label: "Docs", href: "/docs" },
{ label: "Careers", href: "/careers" },
]
/**
* The copyright line. The year is the sample world's — the year of the kit's
* `REFERENCE_DATE`, which the section's test holds it to — and never the
* clock's, so the server and the browser print the same line.
*/
export const COPYRIGHT = { year: 2026, holder: "Northwind Analytics Ltd." } as constnpx shadcn@latest add @vibra/footer-01Sitemap footer
footer-02The company in a sentence and an address to write to, beside the whole site in four named columns, over a row with the copyright and the legal pages.
import * as React from "react"
import Link from "next/link"
import { LogoMark } from "@/components/ui/logo-mark"
import { StatusBadge } from "@/components/ui/status-badge"
import { BRAND, COLUMNS, COPYRIGHT, LEGAL, type FooterColumn, type FooterLink } from "./footer-02.data"
const LINK =
"inline-flex items-center gap-2 rounded-sm text-muted-foreground transition-colors duration-(--duration-fast) ease-(--ease-standard) focus-ring hover:text-foreground"
/**
* A link and what the site says about it: a word it is marked with while it
* is news, in a small pill in the success tone, or a count as a plain figure
* that a screen reader hears with its noun — "Careers 4 open roles".
*/
function SitemapLink({ link }: { link: FooterLink }) {
return (
<Link href={link.href} className={LINK}>
{link.label}
{/* A space the flex row does not draw but a screen reader reads, so the
name is "Cohort reports New" rather than one run-on word. */}
{link.tag || link.count ? " " : null}
{link.tag ? <StatusBadge status={link.tag} variant="success" size="sm" /> : null}
{link.count ? (
<span className="text-faint-foreground tabular-nums">
{link.count.value}{" "}
<span className="sr-only">{link.count.noun}</span>
</span>
) : null}
</Link>
)
}
/** One column of the sitemap: a nav named by its own heading. */
function SitemapColumn({ column }: { column: FooterColumn }) {
const headingId = React.useId()
return (
<nav aria-labelledby={headingId} className="min-w-0">
<h2 id={headingId} className="text-sm font-medium text-foreground">
{column.heading}
</h2>
<ul className="mt-4 flex flex-col gap-2 text-sm/6">
{column.links.map((link) => (
<li key={link.href}>
<SitemapLink link={link} />
</li>
))}
</ul>
</nav>
)
}
/**
* The whole site in four columns beside the company in one sentence and the
* address that reaches a person, over a closing row with the copyright and
* the legal pages. Every column is a nav named by its heading, so a screen
* reader's landmark list reads Product, Teams, Resources, Company, Legal.
*
* Two columns up on a phone, four from `sm`, and from `lg` the company sits
* to their left. A server component; every word is in `./footer-02.data`.
*/
export function Footer02() {
return (
<footer
data-section="footer-02"
data-slot="section"
className="w-full border-t border-rule bg-background px-4 pt-12 pb-8 text-foreground md:px-6 md:pt-16"
>
<div className="mx-auto max-w-7xl">
<div className="grid gap-12 lg:grid-cols-12 lg:gap-8">
<div className="flex max-w-sm flex-col items-start gap-4 lg:col-span-4">
<Link href={BRAND.href} aria-label={BRAND.homeLabel} className="flex w-fit rounded-md focus-ring">
<LogoMark name="Northwind" />
</Link>
<p className="text-sm/6 text-pretty text-muted-foreground">{BRAND.summary}</p>
<a
href={BRAND.contact.href}
className="rounded-sm text-sm/6 font-medium text-foreground underline-offset-4 focus-ring hover:underline"
>
{BRAND.contact.label}
</a>
</div>
<div className="grid grid-cols-2 gap-x-6 gap-y-10 sm:grid-cols-4 lg:col-span-8">
{COLUMNS.map((column) => (
<SitemapColumn key={column.heading} column={column} />
))}
</div>
</div>
<div className="mt-12 flex flex-col gap-4 border-t border-border pt-6 md:flex-row md:items-center md:justify-between">
<p className="text-sm/6 text-muted-foreground">
© {COPYRIGHT.year} {COPYRIGHT.holder}
</p>
<nav aria-label={LEGAL.heading}>
<ul className="flex flex-wrap gap-x-6 gap-y-2 text-sm/6">
{LEGAL.links.map((link) => (
<li key={link.href}>
<Link href={link.href} className={LINK}>
{link.label}
</Link>
</li>
))}
</ul>
</nav>
</div>
</div>
</footer>
)
}
export default Footer02/**
* Every word `footer-02` sets: the company in a sentence and an address to
* write to, the sitemap in four columns, the legal pages and the copyright.
* The links name routes of your own site; replace them and the footer walks
* yours.
*/
export type FooterLink = {
label: string
href: string
/** A word the site marks the page with while it is news: "New", "Beta". */
tag?: string
/** A figure beside the link, and what it counts, e.g. the open roles on Careers. */
count?: { value: number; noun: string }
}
export type FooterColumn = { heading: string; links: FooterLink[] }
export const BRAND = {
href: "/",
homeLabel: "Northwind home",
summary:
"Northwind puts revenue, product usage and support numbers in one workspace, for the teams who review them every week.",
contact: { label: "hello@northwind.example", href: "mailto:hello@northwind.example" },
} as const
export const COLUMNS: FooterColumn[] = [
{
heading: "Product",
links: [
{ label: "Overview", href: "/product" },
{ label: "Dashboards", href: "/product/dashboards" },
{ label: "Cohort reports", href: "/product/cohorts", tag: "New" },
{ label: "Forecasts", href: "/product/forecasts" },
{ label: "Alerts", href: "/product/alerts" },
{ label: "Pricing", href: "/pricing" },
],
},
{
heading: "Teams",
links: [
{ label: "Finance", href: "/teams/finance" },
{ label: "Revenue operations", href: "/teams/revenue-operations" },
{ label: "Product", href: "/teams/product" },
{ label: "Customer success", href: "/teams/customer-success" },
{ label: "Support", href: "/teams/support" },
],
},
{
heading: "Resources",
links: [
{ label: "Documentation", href: "/docs" },
{ label: "API reference", href: "/docs/api" },
{ label: "Guides", href: "/guides" },
{ label: "Templates", href: "/templates" },
{ label: "Changelog", href: "/changelog" },
],
},
{
heading: "Company",
links: [
{ label: "About", href: "/about" },
{ label: "Customers", href: "/customers" },
{ label: "Careers", href: "/careers", count: { value: 4, noun: "open roles" } },
{ label: "Press", href: "/press" },
{ label: "Contact", href: "/contact" },
],
},
]
export const LEGAL: FooterColumn = {
heading: "Legal",
links: [
{ label: "Terms", href: "/legal/terms" },
{ label: "Privacy", href: "/legal/privacy" },
{ label: "Cookies", href: "/legal/cookies" },
{ label: "Security", href: "/security" },
],
}
/**
* The copyright line. The year is the sample world's — the year of the kit's
* `REFERENCE_DATE`, which the section's test holds it to — and never the
* clock's, so the server and the browser print the same line.
*/
export const COPYRIGHT = { year: 2026, holder: "Northwind Analytics Ltd." } as constnpx shadcn@latest add @vibra/footer-02Footer with a newsletter sign-up
footer-03The newsletter — what arrives and when — with a field that refuses a bad address in its own words and confirms a good one, beside three columns of links.
import * as React from "react"
import Link from "next/link"
import { LogoMark } from "@/components/ui/logo-mark"
import { NewsletterSignup } from "./footer-03.client"
import { BRAND, COLUMNS, COPYRIGHT, LEGAL, NEWSLETTER, type FooterColumn } from "./footer-03.data"
const LINK =
"rounded-sm text-muted-foreground transition-colors duration-(--duration-fast) ease-(--ease-standard) focus-ring hover:text-foreground"
/** One column of links: a nav named by its own heading. */
function LinkColumn({ column }: { column: FooterColumn }) {
const headingId = React.useId()
return (
<nav aria-labelledby={headingId} className="min-w-0">
<h2 id={headingId} className="text-sm font-medium text-foreground">
{column.heading}
</h2>
<ul className="mt-4 flex flex-col gap-2 text-sm/6">
{column.links.map((link) => (
<li key={link.href}>
<Link href={link.href} className={LINK}>
{link.label}
</Link>
</li>
))}
</ul>
</nav>
)
}
/**
* A footer that asks for one thing before it lists the rest: the newsletter —
* what arrives, when, and the field that signs a reader up — on the left, the
* three columns of links on the right, and the mark, the copyright and the
* legal pages closing it. The sign-up is a form named by its heading, so it
* is a landmark of its own.
*
* The form is the one island; this file is a server component and every word,
* the form's refusals and its confirmation included, is in `./footer-03.data`.
*/
export function Footer03() {
const newsletterId = React.useId()
return (
<footer
data-section="footer-03"
data-slot="section"
className="w-full border-t border-rule bg-background px-4 pt-12 pb-8 text-foreground md:px-6 md:pt-16"
>
<div className="mx-auto max-w-7xl">
<div className="grid gap-12 lg:grid-cols-12 lg:gap-8">
<div className="max-w-md lg:col-span-5">
<h2 id={newsletterId} className="text-base font-semibold">
{NEWSLETTER.heading}
</h2>
<p className="mt-2 text-sm/6 text-pretty text-muted-foreground">{NEWSLETTER.description}</p>
<div className="mt-6">
<NewsletterSignup copy={NEWSLETTER.form} labelledBy={newsletterId} />
</div>
</div>
<div className="grid grid-cols-2 gap-x-6 gap-y-10 sm:grid-cols-3 lg:col-span-6 lg:col-start-7">
{COLUMNS.map((column) => (
<LinkColumn key={column.heading} column={column} />
))}
</div>
</div>
<div className="mt-12 flex flex-col gap-4 border-t border-border pt-6 md:flex-row md:items-center md:justify-between">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-5">
<Link href={BRAND.href} aria-label={BRAND.homeLabel} className="flex w-fit rounded-md focus-ring">
<LogoMark name="Northwind" size="sm" />
</Link>
<p className="text-sm/6 text-muted-foreground">
© {COPYRIGHT.year} {COPYRIGHT.holder}
</p>
</div>
<nav aria-label={LEGAL.heading}>
<ul className="flex flex-wrap gap-x-6 gap-y-2 text-sm/6">
{LEGAL.links.map((link) => (
<li key={link.href}>
<Link href={link.href} className={LINK}>
{link.label}
</Link>
</li>
))}
</ul>
</nav>
</div>
</div>
</footer>
)
}
export default Footer03"use client"
import * as React from "react"
import { CircleCheckIcon } from "lucide-react"
import { isEmail } from "@/lib/validation"
import { Button } from "@/components/ui/button"
import { FieldError } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import type { NewsletterForm } from "./footer-03.data"
/**
* The sign-up: one labelled field and one button, named as a form by the
* heading the footer gives it. The browser's own validation is off so the
* form refuses in its own words — an empty field and a malformed address each
* get a message tied to the field, which is marked invalid and takes focus
* back. An accepted address clears the field and is confirmed in the status
* line under it, which is always mounted so the confirmation is announced as
* it arrives. The refusal and the confirmation never share a slot.
*
* It keeps no list: wire the accepted address to your mailing provider where
* `setSubscribed` is called.
*/
export function NewsletterSignup({ copy, labelledBy }: { copy: NewsletterForm; labelledBy: string }) {
const inputId = React.useId()
const errorId = React.useId()
const inputRef = React.useRef<HTMLInputElement>(null)
const [error, setError] = React.useState<string | null>(null)
const [subscribed, setSubscribed] = React.useState<string | null>(null)
function submit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
const email = String(new FormData(event.currentTarget).get("email") ?? "").trim()
const problem = email === "" ? copy.empty : isEmail(email) ? null : copy.invalid
setError(problem)
if (problem) {
setSubscribed(null)
inputRef.current?.focus()
return
}
setSubscribed(email)
event.currentTarget.reset()
}
return (
<form noValidate onSubmit={submit} aria-labelledby={labelledBy} className="flex w-full flex-col gap-2">
<Label htmlFor={inputId}>{copy.label}</Label>
<div className="flex flex-col gap-2 sm:flex-row">
<Input
ref={inputRef}
id={inputId}
name="email"
type="email"
autoComplete="email"
placeholder={copy.placeholder}
aria-invalid={error ? true : undefined}
aria-describedby={error ? errorId : undefined}
className="h-9 sm:flex-1"
/>
<Button type="submit" size="lg">
{copy.submit}
</Button>
</div>
{error ? <FieldError id={errorId}>{error}</FieldError> : null}
<p role="status" aria-live="polite" className="min-h-5 text-sm">
{subscribed ? (
<span className="inline-flex items-start gap-2 text-foreground">
<CircleCheckIcon aria-hidden="true" className="mt-0.5 size-4 shrink-0 text-success" />
{copy.subscribed.replace("{email}", subscribed)}
</span>
) : null}
</p>
</form>
)
}/**
* Every word `footer-03` sets: the newsletter it signs readers up to — what
* arrives, how often, and what the form says back — the three columns of
* links, the legal pages and the copyright. The links name routes of your own
* site, so replacing this file is the whole edit.
*/
export type FooterLink = { label: string; href: string }
export type FooterColumn = { heading: string; links: FooterLink[] }
/** What the sign-up form says, handed to its island as plain props. */
export type NewsletterForm = {
label: string
placeholder: string
submit: string
/** The refusal for a field left empty. */
empty: string
/** The refusal for something that is not an address. */
invalid: string
/** The confirmation; `{email}` is replaced with the address that was signed up. */
subscribed: string
}
export const NEWSLETTER = {
heading: "The Northwind monthly",
description:
"One email on the first Tuesday of each month: what shipped, one team's setup in detail, and the numbers behind it. Every issue has a one-click unsubscribe.",
form: {
label: "Email address",
placeholder: "name@example.com",
submit: "Subscribe",
empty: "Enter your email address to subscribe.",
invalid: "Enter an email address in the format name@example.com.",
subscribed: "Subscribed. The next issue goes to {email}.",
} satisfies NewsletterForm,
}
export const BRAND = { href: "/", homeLabel: "Northwind home" } as const
export const COLUMNS: FooterColumn[] = [
{
heading: "Product",
links: [
{ label: "Dashboards", href: "/product/dashboards" },
{ label: "Cohort reports", href: "/product/cohorts" },
{ label: "Forecasts", href: "/product/forecasts" },
{ label: "Pricing", href: "/pricing" },
],
},
{
heading: "Resources",
links: [
{ label: "Documentation", href: "/docs" },
{ label: "Guides", href: "/guides" },
{ label: "Past issues", href: "/newsletter" },
{ label: "Changelog", href: "/changelog" },
],
},
{
heading: "Company",
links: [
{ label: "About", href: "/about" },
{ label: "Customers", href: "/customers" },
{ label: "Careers", href: "/careers" },
{ label: "Contact", href: "/contact" },
],
},
]
export const LEGAL: FooterColumn = {
heading: "Legal",
links: [
{ label: "Terms", href: "/legal/terms" },
{ label: "Privacy", href: "/legal/privacy" },
{ label: "Cookies", href: "/legal/cookies" },
],
}
/**
* The copyright line. The year is the sample world's — the year of the kit's
* `REFERENCE_DATE`, which the section's test holds it to — and never the
* clock's, so the server and the browser print the same line.
*/
export const COPYRIGHT = { year: 2026, holder: "Northwind Analytics Ltd." } as constnpx shadcn@latest add @vibra/footer-03Footer with a full-width wordmark
footer-04One sentence and three columns of links, then the company's name set across the full width of the page, then the copyright, the legal pages and a way back to the top.
import * as React from "react"
import Link from "next/link"
import { BACK_TO_TOP, COLUMNS, COPYRIGHT, LEGAL, STATEMENT, WORDMARK, type FooterColumn } from "./footer-04.data"
const LINK =
"rounded-sm text-muted-foreground transition-colors duration-(--duration-fast) ease-(--ease-standard) focus-ring hover:text-foreground"
/**
* The wordmark's box, in its own units, measured on the kit's face (DM Sans
* bold at `SIZE`, tracked tight): the ink of the name starts `MIN_X` in from
* its origin and runs `WIDTH` across, from the cap height down to the
* overshoot under `BASELINE`. The viewBox is that ink, so the SVG scales the
* name edge to edge of the column at every width with no type size of its
* own. `ADVANCE` is the name's own length in that face, handed to
* `textLength` — nothing changes for DM Sans, and any other face is fitted to
* the same box rather than overflowing it.
*/
const WORDMARK_BOX = { MIN_X: 14, WIDTH: 966, HEIGHT: 148, BASELINE: 145, SIZE: 200, ADVANCE: 988 } as const
/** One column of links: a nav named by its own heading. */
function LinkColumn({ column }: { column: FooterColumn }) {
const headingId = React.useId()
return (
<nav aria-labelledby={headingId} className="min-w-0">
<h2 id={headingId} className="text-sm font-medium text-foreground">
{column.heading}
</h2>
<ul className="mt-4 flex flex-col gap-2 text-sm/6">
{column.links.map((link) => (
<li key={link.href}>
<Link href={link.href} className={LINK}>
{link.label}
</Link>
</li>
))}
</ul>
</nav>
)
}
/**
* A footer that signs off with the name: the company's one sentence and three
* columns of links, then the name set across the full width of the page, then
* the copyright, the legal pages and a way back up.
*
* The name is an SVG whose text is fitted to its box, so it spans the column
* from a phone to a wide screen without a type size of its own; it is a
* picture, hidden from assistive technology, because the copyright line
* already says it in words. A server component; every word is in
* `./footer-04.data`.
*/
export function Footer04() {
const { MIN_X, WIDTH, HEIGHT, BASELINE, SIZE, ADVANCE } = WORDMARK_BOX
return (
<footer
data-section="footer-04"
data-slot="section"
className="w-full border-t border-rule bg-background px-4 pt-12 pb-8 text-foreground md:px-6 md:pt-16"
>
<div className="mx-auto max-w-7xl">
<div className="grid gap-12 lg:grid-cols-12 lg:gap-8">
<p className="max-w-md text-xl/8 font-semibold text-balance lg:col-span-5">{STATEMENT}</p>
<div className="grid grid-cols-2 gap-x-6 gap-y-10 sm:grid-cols-3 lg:col-span-6 lg:col-start-7">
{COLUMNS.map((column) => (
<LinkColumn key={column.heading} column={column} />
))}
</div>
</div>
<svg
data-slot="footer-wordmark"
aria-hidden="true"
focusable="false"
viewBox={`${MIN_X} 0 ${WIDTH} ${HEIGHT}`}
className="mt-16 block h-auto w-full fill-current text-foreground md:mt-24"
>
<text
x="0"
y={BASELINE}
fontSize={SIZE}
textLength={ADVANCE}
lengthAdjust="spacingAndGlyphs"
className="font-display font-bold tracking-tight"
>
{WORDMARK}
</text>
</svg>
<div className="mt-8 flex flex-col gap-4 border-t border-border pt-6 md:flex-row md:items-center md:justify-between">
<p className="text-sm/6 text-muted-foreground">
© {COPYRIGHT.year} {COPYRIGHT.holder}
</p>
<div className="flex flex-wrap items-center gap-x-6 gap-y-2 text-sm/6">
<nav aria-label={LEGAL.heading}>
<ul className="flex flex-wrap gap-x-6 gap-y-2">
{LEGAL.links.map((link) => (
<li key={link.href}>
<Link href={link.href} className={LINK}>
{link.label}
</Link>
</li>
))}
</ul>
</nav>
<a href={BACK_TO_TOP.href} className={LINK}>
{BACK_TO_TOP.label}
</a>
</div>
</div>
</div>
</footer>
)
}
export default Footer04/**
* Every word `footer-04` sets: the line the company signs off with, three
* columns of links, the name drawn across the foot of the page, the legal
* pages and the copyright. The links name routes of your own site, so
* replacing this file is the whole edit.
*/
export type FooterLink = { label: string; href: string }
export type FooterColumn = { heading: string; links: FooterLink[] }
/** The sentence the footer opens on, set larger than the links under it. */
export const STATEMENT = "Revenue, product usage and support, in one workspace the whole team reads the same way."
/** The name set across the full width of the page, as a picture: the copyright line says it in words. */
export const WORDMARK = "Northwind"
export const COLUMNS: FooterColumn[] = [
{
heading: "Product",
links: [
{ label: "Dashboards", href: "/product/dashboards" },
{ label: "Cohort reports", href: "/product/cohorts" },
{ label: "Forecasts", href: "/product/forecasts" },
{ label: "Pricing", href: "/pricing" },
],
},
{
heading: "Company",
links: [
{ label: "About", href: "/about" },
{ label: "Customers", href: "/customers" },
{ label: "Careers", href: "/careers" },
{ label: "Contact", href: "/contact" },
],
},
{
heading: "Resources",
links: [
{ label: "Documentation", href: "/docs" },
{ label: "Guides", href: "/guides" },
{ label: "Changelog", href: "/changelog" },
{ label: "Status", href: "/status" },
],
},
]
export const LEGAL: FooterColumn = {
heading: "Legal",
links: [
{ label: "Terms", href: "/legal/terms" },
{ label: "Privacy", href: "/legal/privacy" },
{ label: "Cookies", href: "/legal/cookies" },
],
}
/** `#top` scrolls a page to its start without needing an element of that name. */
export const BACK_TO_TOP: FooterLink = { label: "Back to top", href: "#top" }
/**
* The copyright line. The year is the sample world's — the year of the kit's
* `REFERENCE_DATE`, which the section's test holds it to — and never the
* clock's, so the server and the browser print the same line.
*/
export const COPYRIGHT = { year: 2026, holder: "Northwind Analytics Ltd." } as constnpx shadcn@latest add @vibra/footer-04Centred footer with the service status
footer-05The mark and the site's pages, centred, over a chip that says whether the service is up — in words beside the tone's dot, with the uptime — and links to the status page.
import * as React from "react"
import Link from "next/link"
import { ChevronRightIcon } from "lucide-react"
import { LogoMark } from "@/components/ui/logo-mark"
import { StatusIndicator } from "@/components/ui/status-indicator"
import { BRAND, COPYRIGHT, LEGAL, NAV, STATUS } from "./footer-05.data"
const LINK =
"rounded-sm text-muted-foreground transition-colors duration-(--duration-fast) ease-(--ease-standard) focus-ring hover:text-foreground"
/**
* A centred footer that answers the question a customer scrolls down for:
* is the service up. Under the mark and the site's pages, a status chip says
* so in words beside the tone's dot, with the uptime behind it, and links to
* the status page; the copyright and the legal pages close it.
*
* The chip is a plain link — the page it leads to is the live record, and a
* footer that re-read it on every render would only be a slower copy. The dot
* is decoration: the words carry the state. A server component; every word is
* in `./footer-05.data`.
*/
export function Footer05() {
return (
<footer
data-section="footer-05"
data-slot="section"
className="w-full border-t border-rule bg-background px-4 py-12 text-foreground md:px-6 md:py-16"
>
<div className="mx-auto flex max-w-7xl flex-col items-center gap-8 text-center">
<Link href={BRAND.href} aria-label={BRAND.homeLabel} className="flex w-fit rounded-md focus-ring">
<LogoMark name="Northwind" size="lg" />
</Link>
<nav aria-label={NAV.label}>
<ul className="flex flex-wrap justify-center gap-x-6 gap-y-2 text-sm/6">
{NAV.links.map((link) => (
<li key={link.href}>
<Link href={link.href} className={LINK}>
{link.label}
</Link>
</li>
))}
</ul>
</nav>
<Link
href={STATUS.href}
className="group/status inline-flex max-w-full items-center gap-2 rounded-full bg-card py-1.5 pr-2.5 pl-3.5 text-sm ring-1 ring-border transition-colors duration-(--duration-fast) ease-(--ease-standard) focus-ring hover:bg-muted"
>
{/* Hidden with its own status word: the label beside it says the
same thing, and a link read as "Healthy All systems normal"
says it twice. */}
<StatusIndicator status={STATUS.tone} aria-hidden="true" />
<span className="font-medium text-foreground">{STATUS.label}</span>{" "}
{/* The uptime from `sm`: on a phone the chip stays one line. */}
<span className="ml-1 hidden text-muted-foreground tabular-nums sm:inline">{STATUS.detail}</span>
<ChevronRightIcon
aria-hidden="true"
className="size-3.5 text-muted-foreground transition-transform duration-(--duration-fast) ease-(--ease-standard) group-hover/status:translate-x-0.5 motion-reduce:transition-none"
/>
</Link>
<div className="flex flex-col items-center gap-3 border-t border-border pt-6 sm:flex-row sm:gap-6">
<p className="text-sm/6 text-muted-foreground">
© {COPYRIGHT.year} {COPYRIGHT.holder}
</p>
<nav aria-label={LEGAL.label}>
<ul className="flex flex-wrap justify-center gap-x-6 gap-y-2 text-sm/6">
{LEGAL.links.map((link) => (
<li key={link.href}>
<Link href={link.href} className={LINK}>
{link.label}
</Link>
</li>
))}
</ul>
</nav>
</div>
</div>
</footer>
)
}
export default Footer05/**
* Every word `footer-05` sets: the pages it links, the state of the service
* with where to read more, the legal pages and the copyright. The links name
* routes of your own site, and the status line is what your status page says
* today — wire `STATUS` to it and the footer reports the live state.
*/
import type { StatusIndicatorStatus } from "@/components/ui/status-indicator"
export type FooterLink = { label: string; href: string }
export const BRAND = { href: "/", homeLabel: "Northwind home" } as const
export const NAV = {
label: "Site",
links: [
{ label: "Product", href: "/product" },
{ label: "Pricing", href: "/pricing" },
{ label: "Customers", href: "/customers" },
{ label: "Docs", href: "/docs" },
{ label: "API", href: "/docs/api" },
{ label: "Security", href: "/security" },
{ label: "Contact", href: "/contact" },
] satisfies FooterLink[],
}
/**
* The service's state as the status page reports it: `tone` picks the
* indicator's colour and `label` says the same thing in words, so the state
* never rests on the colour alone.
*/
export const STATUS: { tone: StatusIndicatorStatus; label: string; detail: string; href: string } = {
tone: "success",
label: "All systems normal",
detail: "99.98% uptime over 90 days",
href: "/status",
}
export const LEGAL = {
label: "Legal",
links: [
{ label: "Terms", href: "/legal/terms" },
{ label: "Privacy", href: "/legal/privacy" },
{ label: "Cookies", href: "/legal/cookies" },
] satisfies FooterLink[],
}
/**
* The copyright line. The year is the sample world's — the year of the kit's
* `REFERENCE_DATE`, which the section's test holds it to — and never the
* clock's, so the server and the browser print the same line.
*/
export const COPYRIGHT = { year: 2026, holder: "Northwind Analytics Ltd." } as constnpx shadcn@latest add @vibra/footer-05Footer with language and theme settings
footer-06Four columns of links over a closing row that holds the copyright and the legal pages on one side and the reader's language and theme on the other.
import * as React from "react"
import Link from "next/link"
import { LogoMark } from "@/components/ui/logo-mark"
import { FooterPreferences } from "./footer-06.client"
import { BRAND, COLUMNS, COPYRIGHT, LEGAL, PREFERENCES, type FooterColumn } from "./footer-06.data"
const LINK =
"rounded-sm text-muted-foreground transition-colors duration-(--duration-fast) ease-(--ease-standard) focus-ring hover:text-foreground"
/** One column of links: a nav named by its own heading. */
function LinkColumn({ column }: { column: FooterColumn }) {
const headingId = React.useId()
return (
<nav aria-labelledby={headingId} className="min-w-0">
<h2 id={headingId} className="text-sm font-medium text-foreground">
{column.heading}
</h2>
<ul className="mt-4 flex flex-col gap-2 text-sm/6">
{column.links.map((link) => (
<li key={link.href}>
<Link href={link.href} className={LINK}>
{link.label}
</Link>
</li>
))}
</ul>
</nav>
)
}
/**
* A footer for a site read in more than one language: four columns of links
* across the full width, then a closing row with the mark, the copyright and
* the legal pages on one side and the reader's settings on the other — the
* language the site is shown in, and light, dark or the system's theme.
*
* The settings are the one island; the theme toggle needs `next-themes`'
* provider, which a shadcn app already mounts. A server component; every
* word, the languages included, is in `./footer-06.data`.
*/
export function Footer06() {
return (
<footer
data-section="footer-06"
data-slot="section"
className="w-full border-t border-rule bg-background px-4 pt-12 pb-8 text-foreground md:px-6 md:pt-16"
>
<div className="mx-auto max-w-7xl">
<div className="grid grid-cols-2 gap-x-6 gap-y-10 md:grid-cols-4 lg:gap-x-8">
{COLUMNS.map((column) => (
<LinkColumn key={column.heading} column={column} />
))}
</div>
<div className="mt-12 flex flex-col gap-6 border-t border-border pt-6 lg:flex-row lg:items-center lg:justify-between">
<div className="flex flex-col gap-3 md:flex-row md:items-center md:gap-6">
<Link href={BRAND.href} aria-label={BRAND.homeLabel} className="flex w-fit rounded-md focus-ring">
<LogoMark name="Northwind" size="sm" />
</Link>
<p className="text-sm/6 text-muted-foreground">
© {COPYRIGHT.year} {COPYRIGHT.holder}
</p>
<nav aria-label={LEGAL.heading}>
<ul className="flex flex-wrap gap-x-6 gap-y-2 text-sm/6">
{LEGAL.links.map((link) => (
<li key={link.href}>
<Link href={link.href} className={LINK}>
{link.label}
</Link>
</li>
))}
</ul>
</nav>
</div>
<FooterPreferences preferences={PREFERENCES} />
</div>
</div>
</footer>
)
}
export default Footer06"use client"
import * as React from "react"
import Link from "next/link"
import { CheckIcon, ChevronDownIcon, GlobeIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { ThemeToggle } from "@/components/ui/theme-toggle"
import type { Preferences } from "./footer-06.data"
/**
* The reader's two settings, side by side: the language, as a menu of links —
* each language is a page of its own, so each item is a link with its
* `hreflang`, named in its own language and read in it — and the theme, as
* the kit's segmented toggle. The menu's button says which language the page
* is in; inside, that one carries the accent's tint, the 500 weight and a
* tick, and `aria-current`, never a stroke. The menu opens upwards, since a
* footer sits at the foot of the page.
*/
export function FooterPreferences({ preferences }: { preferences: Preferences }) {
const { languageLabel, locales, current } = preferences
const active = locales.find((locale) => locale.code === current) ?? locales[0]
return (
<div data-slot="footer-preferences" className="flex flex-wrap items-center gap-3">
<DropdownMenu>
<DropdownMenuTrigger
render={<Button variant="outline" size="sm" aria-label={`${languageLabel}: ${active.name}`} />}
>
<GlobeIcon aria-hidden="true" className="text-muted-foreground" />
<span lang={active.code}>{active.name}</span>
<ChevronDownIcon aria-hidden="true" className="text-muted-foreground" />
</DropdownMenuTrigger>
<DropdownMenuContent side="top" align="start" className="w-auto min-w-44">
{locales.map((locale) => {
const isCurrent = locale.code === active.code
return (
<DropdownMenuItem
key={locale.code}
render={
<Link
href={locale.href}
hrefLang={locale.code}
lang={locale.code}
aria-current={isCurrent ? "true" : undefined}
/>
}
className={cn(isCurrent && "bg-brand-muted font-medium")}
>
{locale.name}
{isCurrent ? <CheckIcon aria-hidden="true" className="ml-auto" /> : null}
</DropdownMenuItem>
)
})}
</DropdownMenuContent>
</DropdownMenu>
<ThemeToggle variant="segmented" size="sm" />
</div>
)
}/**
* Every word `footer-06` sets: four columns of links, the languages the site
* is published in and the one this page is in, the legal pages and the
* copyright. Each language is a page of its own, so each one is a link —
* replace them with your localised routes and the menu walks them.
*/
export type FooterLink = { label: string; href: string }
export type FooterColumn = { heading: string; links: FooterLink[] }
/** A language the site is published in: its BCP 47 tag, its name in itself, and its home page. */
export type Locale = { code: string; name: string; href: string }
/** What the preferences row says and offers, handed to its island as plain props. */
export type Preferences = {
/** Read before the current language on the menu's button: "Language: English (UK)". */
languageLabel: string
locales: Locale[]
/** The `code` of the language this page is in. */
current: string
}
export const BRAND = { href: "/", homeLabel: "Northwind home" } as const
export const COLUMNS: FooterColumn[] = [
{
heading: "Product",
links: [
{ label: "Dashboards", href: "/product/dashboards" },
{ label: "Cohort reports", href: "/product/cohorts" },
{ label: "Forecasts", href: "/product/forecasts" },
{ label: "Alerts", href: "/product/alerts" },
{ label: "Pricing", href: "/pricing" },
],
},
{
heading: "Teams",
links: [
{ label: "Finance", href: "/teams/finance" },
{ label: "Revenue operations", href: "/teams/revenue-operations" },
{ label: "Customer success", href: "/teams/customer-success" },
{ label: "Support", href: "/teams/support" },
],
},
{
heading: "Resources",
links: [
{ label: "Documentation", href: "/docs" },
{ label: "API reference", href: "/docs/api" },
{ label: "Guides", href: "/guides" },
{ label: "Changelog", href: "/changelog" },
],
},
{
heading: "Company",
links: [
{ label: "About", href: "/about" },
{ label: "Customers", href: "/customers" },
{ label: "Careers", href: "/careers" },
{ label: "Contact", href: "/contact" },
],
},
]
export const PREFERENCES: Preferences = {
languageLabel: "Language",
current: "en-GB",
locales: [
{ code: "en-GB", name: "English (UK)", href: "/" },
{ code: "en-US", name: "English (US)", href: "/en-us" },
{ code: "de", name: "Deutsch", href: "/de" },
{ code: "fr", name: "Français", href: "/fr" },
{ code: "es", name: "Español", href: "/es" },
{ code: "pt-PT", name: "Português", href: "/pt" },
{ code: "ja", name: "日本語", href: "/ja" },
],
}
export const LEGAL: FooterColumn = {
heading: "Legal",
links: [
{ label: "Terms", href: "/legal/terms" },
{ label: "Privacy", href: "/legal/privacy" },
{ label: "Cookies", href: "/legal/cookies" },
],
}
/**
* The copyright line. The year is the sample world's — the year of the kit's
* `REFERENCE_DATE`, which the section's test holds it to — and never the
* clock's, so the server and the browser print the same line.
*/
export const COPYRIGHT = { year: 2026, holder: "Northwind Analytics Ltd." } as constnpx shadcn@latest add @vibra/footer-06Mega footer with a trial strip
footer-07A strip that asks for the trial once more, then the company and three ways to reach a person beside the whole site in five columns, then the registered name and every legal page.
import * as React from "react"
import Link from "next/link"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
import { LogoMark } from "@/components/ui/logo-mark"
import { BRAND, CONTACT, COLUMNS, COPYRIGHT, CTA, LEGAL, type FooterColumn } from "./footer-07.data"
const LINK =
"rounded-sm text-muted-foreground transition-colors duration-(--duration-fast) ease-(--ease-standard) focus-ring hover:text-foreground"
/** One column of the sitemap: a nav named by its own heading. */
function LinkColumn({ column }: { column: FooterColumn }) {
const headingId = React.useId()
return (
<nav aria-labelledby={headingId} className="min-w-0">
<h2 id={headingId} className="text-sm font-medium text-foreground">
{column.heading}
</h2>
<ul className="mt-4 flex flex-col gap-2 text-sm/6">
{column.links.map((link) => (
<li key={link.href}>
<Link href={link.href} className={LINK}>
{link.label}
</Link>
</li>
))}
</ul>
</nav>
)
}
/**
* The footer of a large marketing site, which asks once more before it lists
* everything: a strip on the surface plane with the trial and its two ways
* in, then the company, the three ways to reach a person and where its
* offices are beside the whole site in five columns, then the copyright, the
* registered name and every legal page.
*
* Five columns from `lg`, three on a tablet and two on a phone; the contact
* lines are a description list, so each address is read with what it is for.
* A server component; every word is in `./footer-07.data`.
*/
export function Footer07() {
return (
<footer
data-section="footer-07"
data-slot="section"
className="w-full border-t border-rule bg-background px-4 pt-12 pb-8 text-foreground md:px-6 md:pt-16"
>
<div className="mx-auto max-w-7xl">
<div className="flex flex-col gap-6 frame p-6 md:flex-row md:items-center md:justify-between md:gap-10 md:p-8">
<div className="max-w-xl">
<h2 className="type-display text-2xl text-balance">
{CTA.heading}
</h2>
<p className="mt-2 text-sm/6 text-pretty text-muted-foreground">{CTA.text}</p>
</div>
<div className="flex shrink-0 flex-wrap gap-3">
<Link href={CTA.primary.href} className={cn(buttonVariants({ size: "lg" }), "h-10 px-5")}>
{CTA.primary.label}
</Link>
<Link
href={CTA.secondary.href}
className={cn(buttonVariants({ variant: "outline", size: "lg" }), "h-10 px-5")}
>
{CTA.secondary.label}
</Link>
</div>
</div>
<div className="mt-12 grid gap-12 md:mt-16 lg:grid-cols-12 lg:gap-8">
<div className="flex max-w-sm flex-col items-start gap-4 lg:col-span-3">
<Link href={BRAND.href} aria-label={BRAND.homeLabel} className="flex w-fit rounded-md focus-ring">
<LogoMark name="Northwind" />
</Link>
<p className="text-sm/6 text-pretty text-muted-foreground">{BRAND.summary}</p>
<div className="mt-2">
<h2 className="text-sm font-medium text-foreground">{CONTACT.heading}</h2>
<dl className="mt-3 grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-sm/6">
{CONTACT.lines.map((line) => (
<div key={line.label} className="contents">
<dt className="text-muted-foreground">{line.label}</dt>
<dd className="min-w-0">
<a
href={line.href}
className="rounded-sm break-all text-foreground tabular-nums underline-offset-4 focus-ring hover:underline"
>
{line.value}
</a>
</dd>
</div>
))}
</dl>
<p className="mt-3 text-sm/6 text-muted-foreground">{CONTACT.offices}</p>
</div>
</div>
<div className="grid grid-cols-2 gap-x-6 gap-y-10 sm:grid-cols-3 lg:col-span-9 lg:grid-cols-5">
{COLUMNS.map((column) => (
<LinkColumn key={column.heading} column={column} />
))}
</div>
</div>
<div className="mt-12 flex flex-col gap-4 border-t border-border pt-6 lg:flex-row lg:items-start lg:justify-between lg:gap-10">
<div className="text-sm/6 text-muted-foreground">
<p>
© {COPYRIGHT.year} {COPYRIGHT.holder}
</p>
<p>{COPYRIGHT.registration}</p>
</div>
<nav aria-label={LEGAL.heading}>
<ul className="flex flex-wrap gap-x-6 gap-y-2 text-sm/6 lg:justify-end">
{LEGAL.links.map((link) => (
<li key={link.href}>
<Link href={link.href} className={LINK}>
{link.label}
</Link>
</li>
))}
</ul>
</nav>
</div>
</div>
</footer>
)
}
export default Footer07/**
* Every word `footer-07` sets: the trial it asks for last, the company and
* how to reach it, the whole site in five columns, the legal pages and the
* registered name. The links name routes of your own site, so replacing this
* file is the whole edit.
*/
export type FooterLink = { label: string; href: string }
export type FooterColumn = { heading: string; links: FooterLink[] }
/** One way to reach a person: what it is for, and the address or number. */
export type ContactLine = { label: string; value: string; href: string }
export const CTA = {
heading: "Start a 14-day trial",
text: "Every feature is on, the data is your own and there is no card to enter. Most teams have a first dashboard within the hour.",
primary: { label: "Start free trial", href: "/sign-up" },
secondary: { label: "Book a demo", href: "/contact" },
} as const
export const BRAND = {
href: "/",
homeLabel: "Northwind home",
summary: "Revenue, product usage and support numbers in one workspace, for the teams who review them every week.",
} as const
export const CONTACT: { heading: string; lines: ContactLine[]; offices: string } = {
heading: "Talk to a person",
lines: [
{ label: "Sales", value: "sales@northwind.example", href: "mailto:sales@northwind.example" },
{ label: "Support", value: "support@northwind.example", href: "mailto:support@northwind.example" },
{ label: "Phone", value: "+44 20 7946 0321", href: "tel:+442079460321" },
],
offices: "Offices in London, Lisbon and Toronto",
}
export const COLUMNS: FooterColumn[] = [
{
heading: "Product",
links: [
{ label: "Overview", href: "/product" },
{ label: "Dashboards", href: "/product/dashboards" },
{ label: "Cohort reports", href: "/product/cohorts" },
{ label: "Forecasts", href: "/product/forecasts" },
{ label: "Alerts", href: "/product/alerts" },
{ label: "Integrations", href: "/integrations" },
],
},
{
heading: "Teams",
links: [
{ label: "Finance", href: "/teams/finance" },
{ label: "Revenue operations", href: "/teams/revenue-operations" },
{ label: "Product", href: "/teams/product" },
{ label: "Customer success", href: "/teams/customer-success" },
{ label: "Support", href: "/teams/support" },
],
},
{
heading: "Compare",
links: [
{ label: "Spreadsheets", href: "/compare/spreadsheets" },
{ label: "BI tools", href: "/compare/bi-tools" },
{ label: "Building in-house", href: "/compare/in-house" },
],
},
{
heading: "Resources",
links: [
{ label: "Documentation", href: "/docs" },
{ label: "API reference", href: "/docs/api" },
{ label: "Guides", href: "/guides" },
{ label: "Templates", href: "/templates" },
{ label: "Webinars", href: "/webinars" },
{ label: "Changelog", href: "/changelog" },
],
},
{
heading: "Company",
links: [
{ label: "About", href: "/about" },
{ label: "Customers", href: "/customers" },
{ label: "Careers", href: "/careers" },
{ label: "Partners", href: "/partners" },
{ label: "Press", href: "/press" },
],
},
]
export const LEGAL: FooterColumn = {
heading: "Legal",
links: [
{ label: "Terms", href: "/legal/terms" },
{ label: "Privacy", href: "/legal/privacy" },
{ label: "Cookies", href: "/legal/cookies" },
{ label: "Security", href: "/security" },
{ label: "Data processing", href: "/legal/dpa" },
{ label: "Accessibility", href: "/accessibility" },
],
}
/**
* The copyright line and the registration under it. The year is the sample
* world's — the year of the kit's `REFERENCE_DATE`, which the section's test
* holds it to — and never the clock's, so the server and the browser print
* the same line.
*/
export const COPYRIGHT = {
year: 2026,
holder: "Northwind Analytics Ltd.",
registration: "Registered in England and Wales, company number 11482039.",
} as constnpx shadcn@latest add @vibra/footer-07Legal row
footer-08The copyright, the registered company and the legal pages on one quiet row: the fine print for a checkout, a sign-in page or an app screen.
import * as React from "react"
import Link from "next/link"
import { COPYRIGHT, LEGAL } from "./footer-08.data"
/**
* The least a page can end on: who publishes it and the pages the law asks
* for, on one quiet row. For the screens that have no sitemap to offer — a
* checkout, a sign-in page, an app's settings — and still owe the reader the
* terms and the privacy policy.
*
* No mark: the page above already carries one. The row breaks into two lines
* below `lg`. A server component; every word is in `./footer-08.data`.
*/
export function Footer08() {
return (
<footer
data-section="footer-08"
data-slot="section"
className="w-full border-t border-rule bg-background px-4 py-6 text-foreground md:px-6"
>
<div className="mx-auto flex max-w-7xl flex-col gap-3 lg:flex-row lg:items-center lg:justify-between lg:gap-10">
<p className="text-sm/6 text-muted-foreground">
© {COPYRIGHT.year} {COPYRIGHT.holder} <span className="text-faint-foreground">{COPYRIGHT.registration}</span>
</p>
<nav aria-label={LEGAL.label}>
<ul className="flex flex-wrap gap-x-5 gap-y-1 text-sm/6">
{LEGAL.links.map((link) => (
<li key={link.href}>
<Link
href={link.href}
className="rounded-sm text-muted-foreground underline-offset-4 transition-colors duration-(--duration-fast) ease-(--ease-standard) focus-ring hover:text-foreground hover:underline"
>
{link.label}
</Link>
</li>
))}
</ul>
</nav>
</div>
</footer>
)
}
export default Footer08/**
* Every word `footer-08` sets: the registered company, the legal pages and
* the copyright — the fine print a checkout, a sign-in page or an app screen
* still owes its reader. The links name routes of your own site, so
* replacing this file is the whole edit.
*/
export type FooterLink = { label: string; href: string }
export const LEGAL = {
label: "Legal",
links: [
{ label: "Terms", href: "/legal/terms" },
{ label: "Privacy", href: "/legal/privacy" },
{ label: "Cookies", href: "/legal/cookies" },
{ label: "Accessibility", href: "/accessibility" },
{ label: "Security", href: "/security" },
] satisfies FooterLink[],
}
/**
* The copyright line and the registration beside it. The year is the sample
* world's — the year of the kit's `REFERENCE_DATE`, which the section's test
* holds it to — and never the clock's, so the server and the browser print
* the same line.
*/
export const COPYRIGHT = {
year: 2026,
holder: "Northwind Analytics Ltd.",
registration: "Registered in England and Wales, company number 11482039.",
} as constnpx shadcn@latest add @vibra/footer-08