Stats
The figures a company leads with, set large beside what each one counts.
Big-number row
stats-01Four figures in one ruled row, each set large with its unit a step back, over what it counts and the window it covers — then the day they were counted and a link to the method.
import * as React from "react"
import Link from "next/link"
import { ArrowRightIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
import { AS_OF, EYEBROW, FIGURES, HEADLINE, LEDE, METHOD } from "./stats-01.data"
/**
* The figures a company leads with, as one row: four numbers set large, each
* with its unit a step back in the muted ink, over what it counts and the
* window and base it covers — then the day they were counted and where the
* method is. Rules, not boxes: the row is divided by hairlines.
*
* The figures are a description list, so each label is read with its number;
* the number is drawn above its label only for the eye. A unit written as a
* symbol ("B") is said in words ("billion") to a screen reader. Two up on a
* phone, four in a row from `lg`. A server component; every word and figure
* is in `./stats-01.data`.
*/
export function Stats01() {
const titleId = React.useId()
return (
<section
data-section="stats-01"
data-slot="section"
aria-labelledby={titleId}
className="w-full bg-background px-4 py-16 text-foreground md:px-6 md:py-24"
>
<div className="mx-auto max-w-7xl">
<div className="grid gap-6 lg:grid-cols-12 lg:items-end lg:gap-16">
<div className="lg:col-span-7">
<p className="type-eyebrow">{EYEBROW}</p>
<h2 id={titleId} className="type-display mt-4 text-3xl text-balance md:text-4xl">
{HEADLINE}
</h2>
</div>
<p className="text-base/7 text-pretty text-muted-foreground lg:col-span-5">{LEDE}</p>
</div>
<dl className="mt-12 grid grid-cols-2 gap-x-6 gap-y-10 border-t border-rule pt-8 md:mt-16 lg:grid-cols-4 lg:gap-x-0 lg:pt-10">
{FIGURES.map((figure) => (
<div
key={figure.label}
className="flex flex-col lg:border-l lg:border-rule lg:px-8 lg:first:border-l-0 lg:first:pl-0 lg:last:pr-0"
>
<dt className="order-2 mt-3 text-sm font-medium text-pretty">{figure.label}</dt>
<dd className="order-1 type-numeral text-4xl sm:text-5xl lg:text-6xl">
{figure.value}
{figure.unit ? (
<>
<span aria-hidden="true" className="ml-0.5 text-2xl font-semibold text-muted-foreground sm:text-3xl">
{figure.unit}
</span>
<span className="sr-only"> {figure.unitSpoken ?? figure.unit}</span>
</>
) : null}
</dd>
<dd className="order-3 mt-1 text-sm text-pretty text-muted-foreground">{figure.note}</dd>
</div>
))}
</dl>
<p className="mt-10 flex flex-wrap items-center gap-x-3 gap-y-1 border-t border-border pt-6 text-sm text-muted-foreground md:mt-12">
{AS_OF}
<Link href={METHOD.href} className={cn(buttonVariants({ variant: "link" }), "group/method h-auto px-0")}>
{METHOD.label}
<ArrowRightIcon
data-icon="inline-end"
aria-hidden="true"
className="transition-transform duration-(--duration-fast) ease-(--ease-standard) group-hover/method:translate-x-0.5 motion-reduce:transition-none"
/>
</Link>
</p>
</div>
</section>
)
}
export default Stats01/**
* Every word and figure `stats-01` sets: four figures Northwind leads with,
* each a value with its unit, what it counts and the window it covers, and
* the day they were counted beside where the method is published. The
* figures are copy: replace them with measurements of your own, and say when
* and how you counted them in `AS_OF` and `METHOD`.
*/
export type StatLink = { label: string; href: string }
export const EYEBROW = "Northwind in numbers"
export const HEADLINE = "Four numbers from a year of Monday reviews"
export const LEDE =
"Counted from our own billing and product data rather than a survey. Each one says what it counts and over which window, and none of them is rounded up."
export type Figure = {
value: string
/** Set smaller after the value: "B", "min", "%". */
unit?: string
/** The unit as a screen reader should say it, where the symbol would not: "billion". */
unitSpoken?: string
label: string
/** The window and the base the figure covers, under its label. */
note: string
}
export const FIGURES: readonly Figure[] = [
{ value: "2,400", label: "Companies on a paid plan", note: "Up from 1,480 in September 2025" },
{ value: "4.1", unit: "B", unitSpoken: "billion", label: "Events counted in August", note: "Billing, product and support events" },
{ value: "11", unit: "min", unitSpoken: "minutes", label: "From sign-up to the first report", note: "Median across 3,960 new workspaces" },
{ value: "99.98", unit: "%", label: "Uptime over the last 90 days", note: "Checked every minute from three regions" },
]
export const AS_OF = "Counted on 1 September 2026."
export const METHOD: StatLink = { label: "How we count", href: "/company/metrics" }npx shadcn@latest add @vibra/stats-01Figures with their twelve months
stats-02Four cards, each a figure, its change since the first of twelve months and those months as a sparkline — bars for a count, a line for a level — so no figure stands on its own.
import * as React from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { MetricDelta } from "@/components/ui/metric-delta"
import { Sparkline } from "@/components/ui/sparkline"
import { EYEBROW, HEADLINE, LEDE, MEASURES, SINCE, WINDOW, figure, yearChange, type Measure } from "./stats-02.data"
/**
* One measure on the kit's framed Card: what it counts on the strip, and on
* the sheet the figure, its change since the run's first month — in a pill
* that knows whether down is the win — and the twelve months it ends on as a
* sparkline, bars for a count per month and a line for a level, between the
* run's first and last month.
*/
function MeasureCard({ measure }: { measure: Measure }) {
return (
<li className="flex min-w-0">
<Card className="w-full">
<CardHeader>
<CardTitle>
<h3>{measure.label}</h3>
</CardTitle>
<CardDescription className="text-pretty">{measure.description}</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-5">
<div className="flex flex-wrap items-center justify-between gap-x-3 gap-y-2">
<p data-slot="measure-figure" className="type-numeral text-4xl">
{figure(measure)}
</p>
<p className="flex items-center gap-1.5 text-xs text-muted-foreground">
<MetricDelta value={yearChange(measure)} positiveIsGood={!measure.lowerIsBetter} variant="pill" size="sm" />
{SINCE}
</p>
</div>
<div>
<Sparkline data={[...measure.months]} type={measure.shape} height={56} />
<p className="mt-2 flex justify-between text-xs text-muted-foreground tabular-nums">
<span>
<span className="sr-only">From </span>
{WINDOW.from}
</span>
<span>
<span className="sr-only">to </span>
{WINDOW.to}
</span>
</p>
</div>
</CardContent>
</Card>
</li>
)
}
/**
* Four figures, each with the year it is the end of: the kit's Card holding
* the number, its change over the twelve months and those months as the kit's
* Sparkline, drawn from the same run the figure is read off — so no figure is
* a claim on its own, and none can disagree with its line.
*
* One card on a phone, two from `sm`, four in a row from `lg`, the cards
* sharing their row's height. A server component; every word and figure is
* in `./stats-02.data`.
*/
export function Stats02() {
const titleId = React.useId()
return (
<section
data-section="stats-02"
data-slot="section"
aria-labelledby={titleId}
className="w-full bg-background px-4 py-16 text-foreground md:px-6 md:py-24"
>
<div className="mx-auto max-w-7xl">
<div className="max-w-2xl">
<p className="type-eyebrow">{EYEBROW}</p>
<h2 id={titleId} className="type-display mt-4 text-3xl text-balance md:text-4xl">
{HEADLINE}
</h2>
<p className="mt-5 text-base/7 text-pretty text-muted-foreground md:text-lg/8">{LEDE}</p>
</div>
<ul className="mt-12 grid gap-4 sm:grid-cols-2 md:mt-16 lg:grid-cols-4">
{MEASURES.map((measure) => (
<MeasureCard key={measure.label} measure={measure} />
))}
</ul>
</div>
</section>
)
}
export default Stats02/**
* Every word and figure `stats-02` sets: four measures of Northwind over the
* last twelve months, each a month-by-month run whose last point is the figure
* the card prints. The figure is formatted from that point by `figure()` and
* the change from the run's first month by `yearChange()`, so a card can never
* print a number its line does not end on.
*/
import { formatCompact, formatNumber } from "@/lib/format"
export const EYEBROW = "The last twelve months"
export const HEADLINE = "A year of Northwind, one month at a time"
export const LEDE =
"Each figure is the last point of the line under it, so you can see how it got there and not only where it stands."
/** The first and last month of every run, under each line. */
export const WINDOW = { from: "Sep 2025", to: "Aug 2026" }
/** Beside the change: where it is counted from. */
export const SINCE = "since Sep 2025"
/** How a run's points are printed: a count abbreviated, a figure with one decimal, or minutes. */
export type FigureFormat = "compact" | "decimal" | "minutes"
export type Measure = {
label: string
description: string
/** Twelve months, September to August; the last is the figure. */
months: readonly number[]
format: FigureFormat
/** Bars for a count per month, a line for a level. */
shape: "bar" | "line"
/** True where going down is the win. */
lowerIsBetter: boolean
}
export const MEASURES: readonly Measure[] = [
{
label: "Reviews sent each Monday",
description: "Scheduled reviews delivered, in the month's last week",
months: [21_500, 22_900, 24_100, 25_000, 26_800, 28_200, 29_900, 31_300, 33_000, 34_600, 36_100, 38_200],
format: "compact",
shape: "bar",
lowerIsBetter: false,
},
{
label: "Questions asked in plain language",
description: "Asked in the product and answered from the model",
months: [184_000, 197_000, 215_000, 236_000, 251_000, 272_000, 290_000, 318_000, 337_000, 361_000, 388_000, 412_000],
format: "compact",
shape: "bar",
lowerIsBetter: false,
},
{
label: "Sources per workspace",
description: "Median connected sources, paying workspaces",
months: [2.1, 2.2, 2.3, 2.4, 2.6, 2.7, 2.8, 2.9, 3.0, 3.1, 3.3, 3.4],
format: "decimal",
shape: "line",
lowerIsBetter: false,
},
{
label: "Minutes to the first report",
description: "Median for workspaces created that month",
months: [26, 24, 23, 21, 19, 18, 17, 15, 14, 13, 12, 11],
format: "minutes",
shape: "line",
lowerIsBetter: true,
},
]
/** A point as the card prints it: 38200 → "38.2K", 3.4 → "3.4", 11 → "11 min". */
export function formatPoint(value: number, format: FigureFormat): string {
if (format === "compact") return formatCompact(value, { maximumFractionDigits: 1 })
if (format === "decimal") return formatNumber(value, { minimumFractionDigits: 1, maximumFractionDigits: 1 })
return `${formatNumber(value, { maximumFractionDigits: 0 })} min`
}
/** The figure a card prints: the last month of its run. */
export function figure(measure: Measure): string {
return formatPoint(measure.months[measure.months.length - 1], measure.format)
}
/** The change across the run, first month to last, as a ratio: 21,500 to 38,200 is +0.777. */
export function yearChange(measure: Measure): number {
const first = measure.months[0]
const last = measure.months[measure.months.length - 1]
return first ? (last - first) / first : 0
}npx shadcn@latest add @vibra/stats-02Figures around a quote
stats-03Two figures either side of one customer's words about the same change — the quote with her face, role and company — and where the figures were measured under them all.
import * as React from "react"
import Link from "next/link"
import { ArrowRightIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { avatarFor } from "@/lib/avatars"
import { getInitials } from "@/lib/format"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { buttonVariants } from "@/components/ui/button"
import { LogoMark } from "@/components/ui/logo-mark"
import { EYEBROW, FIGURES, HEADLINE, QUOTE, SOURCE, type Figure } from "./stats-03.data"
/** Two figures, each set large over what it counts: a description list, drawn figure-first. */
function FigureList({ figures, className }: { figures: readonly Figure[]; className?: string }) {
return (
<dl className={cn("grid grid-cols-2 gap-x-6 md:grid-cols-1", className)}>
{figures.map((figure) => (
<div key={figure.label} className="flex flex-col-reverse gap-2 border-t border-rule py-6 md:py-8">
<dt className="text-sm/6 text-pretty text-muted-foreground">{figure.label}</dt>
<dd className="type-numeral text-4xl md:text-5xl">{figure.value}</dd>
</div>
))}
</dl>
)
}
/**
* Figures around a quote: two figures either side of one customer's words
* about the same change, so the medians and the voice back each other up.
* The quote is a `figure` on the frame recipe — the company's mark above it
* as decoration, her face, name, role and company as its caption, and a link
* to the whole story — and the figures are description lists read figure
* last, drawn figure-first. Where they were measured is said under them all.
*
* Three columns from `lg`, the quote in the middle; on a tablet the figures
* stand in two columns over the quote; on a phone two figures, the quote,
* then the other two. A server component; every word and figure is in
* `./stats-03.data`.
*/
export function Stats03() {
const titleId = React.useId()
return (
<section
data-section="stats-03"
data-slot="section"
aria-labelledby={titleId}
className="w-full bg-background px-4 py-16 text-foreground md:px-6 md:py-24"
>
<div className="mx-auto max-w-7xl">
<div className="mx-auto max-w-2xl text-center">
<p className="type-eyebrow">{EYEBROW}</p>
<h2 id={titleId} className="type-display mt-4 text-3xl text-balance md:text-4xl">
{HEADLINE}
</h2>
</div>
<div className="mt-12 grid gap-x-10 gap-y-8 md:mt-16 md:grid-cols-2 lg:grid-cols-12 lg:items-center">
<FigureList figures={FIGURES.slice(0, 2)} className="lg:col-span-3" />
<figure className="frame flex flex-col p-6 sm:p-8 md:col-span-2 md:row-start-2 lg:col-span-6 lg:row-start-auto lg:p-10">
<LogoMark name={QUOTE.company} aria-hidden="true" className="text-muted-foreground" />
<blockquote className="mt-6 text-xl/8 font-medium text-pretty md:text-2xl/9">
<p>“{QUOTE.text}”</p>
</blockquote>
<figcaption className="mt-8 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3">
<Avatar size="lg">
<AvatarImage src={avatarFor(QUOTE.name)} alt="" />
<AvatarFallback>{getInitials(QUOTE.name)}</AvatarFallback>
</Avatar>
<div className="text-sm">
<p className="font-medium">{QUOTE.name}</p>
<p className="text-muted-foreground">
{QUOTE.role}, {QUOTE.company}
</p>
</div>
</div>
<Link href={QUOTE.story.href} className={cn(buttonVariants({ variant: "link" }), "group/story h-auto self-start px-0 sm:self-auto")}>
{QUOTE.story.label}
<ArrowRightIcon
data-icon="inline-end"
aria-hidden="true"
className="transition-transform duration-(--duration-fast) ease-(--ease-standard) group-hover/story:translate-x-0.5 motion-reduce:transition-none"
/>
</Link>
</figcaption>
</figure>
<FigureList figures={FIGURES.slice(2)} className="lg:col-span-3" />
</div>
<p className="mx-auto mt-10 max-w-2xl text-center text-sm text-balance text-muted-foreground tabular-nums md:mt-12">{SOURCE}</p>
</div>
</section>
)
}
export default Stats03/**
* Every word and figure `stats-03` sets: four figures on what changes in a
* team's first quarter on Northwind, where they were measured, and one
* customer's words about the same change, with her face, role and company.
* The figures are copy: replace them with medians you measured, and the quote
* with a customer who agreed to be quoted.
*/
import type { LogoName } from "@/components/ui/logo-mark"
export type StatLink = { label: string; href: string }
export const EYEBROW = "The first quarter"
export const HEADLINE = "What changes in a team's first three months"
export const SOURCE =
"Medians across 1,284 workspaces between April and June 2026, each measured against the team's own first month."
/** A figure and what it counts, read in that order: "4.1 days — less time to close the month". */
export type Figure = { value: string; label: string }
/** Two figures stand either side of the quote: the first two before it, the last two after. */
export const FIGURES: readonly Figure[] = [
{ value: "4.1 days", label: "less time to close the month" },
{ value: "31%", label: "fewer spreadsheet exports each week" },
{ value: "2.6×", label: "as many people read the Monday review" },
{ value: "46%", label: "fewer requests to the data team" },
]
export const QUOTE = {
text: "We stopped arguing about whose number was right. The review takes twenty minutes now, and all twenty are about what to do next.",
name: "Ines Carvalho",
role: "VP Finance",
company: "Harbour" as LogoName,
story: { label: "Read Harbour's story", href: "/customers/harbour" } satisfies StatLink,
}npx shadcn@latest add @vibra/stats-03Quarter against a year before
stats-04Six figures in a ruled grid, each where it stands now, its change as a pill toned by which way is the win, and where it stood the same quarter a year before — one of them a loss, published anyway.
import * as React from "react"
import { MetricDelta } from "@/components/ui/metric-delta"
import { EYEBROW, HEADLINE, LEDE, MEASURES, PERIODS, change, from } from "./stats-04.data"
/**
* A quarter against the same quarter a year before: six figures in a ruled
* grid, each what it measures, where it stands now set large, the change as
* the kit's MetricDelta pill — which says the direction in words as well as
* with the arrow, and takes its tone from whether down is the win — and where
* it stood a year before. The change is worked out from the two values, so
* it can never disagree with them.
*
* Each figure is a description list entry read as what it measures, the
* figure and its change, then where it started. One column on a phone, two
* from `sm`, three from `lg`, each figure under a rule. A server component;
* every word and figure is in `./stats-04.data`.
*/
export function Stats04() {
const titleId = React.useId()
return (
<section
data-section="stats-04"
data-slot="section"
aria-labelledby={titleId}
className="w-full bg-background px-4 py-16 text-foreground md:px-6 md:py-24"
>
<div className="mx-auto max-w-7xl">
<div className="grid gap-6 lg:grid-cols-12 lg:items-end lg:gap-16">
<div className="lg:col-span-7">
<p className="type-eyebrow tabular-nums">
{EYEBROW}, {PERIODS.now}
</p>
<h2 id={titleId} className="type-display mt-4 text-3xl text-balance md:text-4xl">
{HEADLINE}
</h2>
</div>
<p className="text-base/7 text-pretty text-muted-foreground lg:col-span-5">{LEDE}</p>
</div>
<dl className="mt-12 grid gap-x-10 sm:grid-cols-2 md:mt-16 lg:grid-cols-3">
{MEASURES.map((measure) => (
<div key={measure.label} className="flex flex-col border-t border-rule pt-6 pb-10">
<dt className="text-sm text-pretty text-muted-foreground">{measure.label}</dt>
<dd className="mt-3 flex flex-wrap items-center gap-x-3 gap-y-2">
<span className="type-numeral text-4xl md:text-5xl">{measure.now.text}</span>
<MetricDelta value={change(measure)} positiveIsGood={!measure.lowerIsBetter} variant="pill" />
</dd>
<dd className="mt-2 text-sm text-muted-foreground tabular-nums">{from(measure)}</dd>
</div>
))}
</dl>
</div>
</section>
)
}
export default Stats04/**
* Every word and figure `stats-04` sets: six measures of Northwind in one
* quarter against the same quarter a year before, each as it reads and as it
* counts. The change on every figure is worked out from the two values by
* `change()`, so it can never disagree with them; `lowerIsBetter` says which
* way is the win, and the pill takes its tone from that.
*/
export const EYEBROW = "Quarterly report"
export const HEADLINE = "The second quarter of 2026, against the same quarter a year before"
export const LEDE =
"Six of the figures we report to our board every quarter, published here as they were reported, including the one that went the wrong way."
export const PERIODS = { now: "Q2 2026", before: "Q2 2025" }
/** Under each figure: `{value}` and `{period}` are the figure a year before and its quarter. */
export const FROM = "from {value} in {period}"
/** A figure as it reads and as it counts: `{ value: 4.1, text: "4.1B" }`. */
export type Figure = { value: number; text: string }
export type Measure = {
label: string
now: Figure
before: Figure
/** True where going down is the win: minutes to a report, minutes to a reply. */
lowerIsBetter: boolean
}
export const MEASURES: readonly Measure[] = [
{ label: "Companies on a paid plan", now: { value: 2400, text: "2,400" }, before: { value: 1690, text: "1,690" }, lowerIsBetter: false },
{ label: "Events counted a month", now: { value: 4.1, text: "4.1B" }, before: { value: 2.2, text: "2.2B" }, lowerIsBetter: false },
{ label: "Reviews sent each Monday", now: { value: 38_200, text: "38,200" }, before: { value: 21_500, text: "21,500" }, lowerIsBetter: false },
{ label: "Median minutes to the first report", now: { value: 11, text: "11 min" }, before: { value: 26, text: "26 min" }, lowerIsBetter: true },
{ label: "Median first reply from support", now: { value: 4, text: "4 min" }, before: { value: 3, text: "3 min" }, lowerIsBetter: true },
{ label: "Integrations you can connect", now: { value: 60, text: "60" }, before: { value: 38, text: "38" }, lowerIsBetter: false },
]
/** The change from a year before to now, as a ratio: 1,690 to 2,400 is +0.42. */
export function change(measure: Measure): number {
return measure.before.value ? (measure.now.value - measure.before.value) / measure.before.value : 0
}
/** The line under a figure: "from 1,690 in Q2 2025". */
export function from(measure: Measure): string {
return FROM.replace("{value}", measure.before.text).replace("{period}", PERIODS.before)
}npx shadcn@latest add @vibra/stats-04One figure and its spread
stats-05A single figure set as large as the page allows — the median time to a first report — beside the spread it is the middle of: the share of workspaces in each band of minutes, the median's band in the ink.
import * as React from "react"
import { cn } from "@/lib/utils"
import { formatPercent } from "@/lib/format"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { BANDS, EYEBROW, FIGURE, HEADLINE, SOURCE, SPREAD, medianBand } from "./stats-05.data"
const percent = (share: number) => formatPercent(share, { maximumFractionDigits: 0 })
/**
* The spread the figure is the middle of: a bar per band of minutes, each as
* tall as its share of the tallest, with the band the median falls in drawn in
* the ink and named above. The picture is for the eye; every band is also one
* item of the list, said in words — "10 to 15 minutes: 24%, the median".
*/
function Spread() {
const median = medianBand()
const tallest = Math.max(...BANDS.map((band) => band.share))
return (
<ol aria-label={SPREAD.description} className="grid grid-cols-7 gap-1.5 sm:gap-3">
{BANDS.map((band, index) => {
// 85% of the plot at most, so the tallest bar's label fits above it.
const height = `${(band.share / tallest) * 85}%`
const isMedian = index === median
return (
<li key={band.label} data-median={isMedian || undefined} className="flex min-w-0 flex-col">
<span className="sr-only">
{band.spoken}: {percent(band.share)}
{isMedian ? `, ${SPREAD.median.toLowerCase()}` : null}
</span>
<div aria-hidden="true" className="relative h-44">
<span
data-slot="spread-bar"
className={cn("absolute inset-x-0 bottom-0 rounded-t-md", isMedian ? "bg-foreground" : "bg-chart-1")}
style={{ height }}
/>
<span
className="absolute inset-x-0 flex flex-col items-center gap-0.5 text-xs tabular-nums"
style={{ bottom: `calc(${height} + 0.375rem)` }}
>
{isMedian ? <span className="font-medium">{SPREAD.median}</span> : null}
<span className={isMedian ? "font-medium" : "text-muted-foreground"}>{percent(band.share)}</span>
</span>
</div>
<span aria-hidden="true" className="truncate border-t border-rule pt-2 text-center text-xs text-muted-foreground tabular-nums">
{band.label}
</span>
</li>
)
})}
</ol>
)
}
/**
* One figure, set as large as the page allows, and the spread it is the
* middle of: the median time from sign-up to a first report beside the kit's
* framed Card holding the share of workspaces in each band of minutes, the
* median's band in the ink. A single figure with its distribution is a claim
* a reader can check — how many were quick, how many slow, and why.
*
* The figure is drawn from its value and unit and said in words ("11
* minutes"). The figure stands left of the chart from `lg`, above it on a
* phone. A server component; every word and figure is in `./stats-05.data`.
*/
export function Stats05() {
const titleId = React.useId()
return (
<section
data-section="stats-05"
data-slot="section"
aria-labelledby={titleId}
className="w-full bg-background px-4 py-16 text-foreground md:px-6 md:py-24"
>
<div className="mx-auto grid max-w-7xl gap-12 lg:grid-cols-12 lg:items-center lg:gap-16">
<div className="lg:col-span-5">
<p className="type-eyebrow">{EYEBROW}</p>
<h2 id={titleId} className="type-display mt-4 text-3xl text-balance md:text-4xl">
{HEADLINE}
</h2>
<p data-slot="hero-figure" className="mt-10 flex items-baseline gap-2">
<span aria-hidden="true" className="type-numeral text-8xl md:text-9xl">
{FIGURE.value}
</span>
<span aria-hidden="true" className="text-3xl font-semibold text-muted-foreground md:text-4xl">
{FIGURE.unit}
</span>
<span className="sr-only">{FIGURE.spoken}</span>
</p>
<p className="mt-3 text-base font-medium text-pretty">{FIGURE.label}</p>
<p className="mt-2 text-sm text-pretty text-muted-foreground tabular-nums">{SOURCE}</p>
</div>
<Card className="lg:col-span-7">
<CardHeader>
<CardTitle>
<h3>{SPREAD.title}</h3>
</CardTitle>
<CardDescription>{SPREAD.description}</CardDescription>
</CardHeader>
<CardContent className="px-4 pt-6 pb-4 sm:px-6">
<Spread />
<p aria-hidden="true" className="mt-3 text-center text-xs text-muted-foreground">
{SPREAD.axis}
</p>
</CardContent>
<CardFooter className="text-sm text-pretty text-muted-foreground">{SPREAD.note}</CardFooter>
</Card>
</div>
</section>
)
}
export default Stats05/**
* Every word and figure `stats-05` sets: one figure — the median time from
* sign-up to a first report — and the spread it is the middle of, as the share
* of workspaces in each band of minutes. `medianBand()` finds the band the
* median falls in from the shares themselves, so the highlighted bar can never
* disagree with the figure it stands under. The shares are copy: replace them
* with your own distribution, and the figure with its median.
*/
export const EYEBROW = "Time to first report"
export const HEADLINE = "Half of new workspaces have a first report within eleven minutes"
/** The one figure, as it is drawn and as it is said. */
export const FIGURE = {
value: "11",
unit: "min",
spoken: "11 minutes",
label: "Median time from sign-up to the first report",
}
export const SOURCE = "Across the 3,960 workspaces created between January and August 2026."
export const SPREAD = {
title: "How long it took",
description: "Share of new workspaces in each band of minutes",
/** Above the band the median falls in. */
median: "Median",
/** The axis under the bars. */
axis: "Minutes from sign-up",
/** What the slowest were waiting on, under the chart. */
note: "A quarter got there in under 7 minutes and three quarters in under 19. Most of the slowest were waiting on a warehouse import.",
}
/** One band of minutes and the share of workspaces whose first report came in it. */
export type Band = { label: string; spoken: string; share: number }
export const BANDS: readonly Band[] = [
{ label: "<5", spoken: "Under 5 minutes", share: 0.14 },
{ label: "5–10", spoken: "5 to 10 minutes", share: 0.29 },
{ label: "10–15", spoken: "10 to 15 minutes", share: 0.24 },
{ label: "15–20", spoken: "15 to 20 minutes", share: 0.13 },
{ label: "20–30", spoken: "20 to 30 minutes", share: 0.1 },
{ label: "30–60", spoken: "30 to 60 minutes", share: 0.06 },
{ label: "60+", spoken: "Over an hour", share: 0.04 },
]
/** The band the median falls in: the first whose running share reaches half. */
export function medianBand(bands: readonly Band[] = BANDS): number {
let running = 0
for (const [index, band] of bands.entries()) {
running += band.share
if (running >= 0.5) return index
}
return bands.length - 1
}npx shadcn@latest add @vibra/stats-05Before and after, measured
stats-06Four measures of a finance team's reporting before Northwind and six months after, each a pair of bars with the figures at their ends and the change in a pill, under a key and over who timed them.
import * as React from "react"
import { cn } from "@/lib/utils"
import { MetricDelta } from "@/components/ui/metric-delta"
import { CHANGE, EYEBROW, HEADLINE, MEASURES, SIDES, SOURCE, change, lengths, type Measure } from "./stats-06.data"
type Side = keyof typeof SIDES
/** Each side's colour: the first two chart slots, grey then the first hue. */
const SWATCH: Record<Side, string> = { before: "bg-chart-1", after: "bg-chart-2" }
/**
* One measure: its name as an h3, then a description list — before, after and
* the change — whose figures stand at the end of bars as long as each figure
* is against the larger of the two. The bars repeat the figures, so they are
* hidden; each side's term is said to a screen reader and keyed for the eye
* above the rows.
*/
function MeasureRow({ measure }: { measure: Measure }) {
const length = lengths(measure)
return (
<li className="grid gap-4 border-t border-rule py-6 lg:grid-cols-12 lg:items-center lg:gap-8">
<h3 className="text-base font-medium text-pretty lg:col-span-4">{measure.label}</h3>
{/* The change column is one width in every row, so the figures beside the bars line up down the band. */}
<dl className="grid grid-cols-[minmax(0,1fr)_5.75rem] items-center gap-x-6 gap-y-2.5 lg:col-span-8">
{(["before", "after"] as const).map((side) => (
<div key={side} className="col-start-1">
<dt className="sr-only">{SIDES[side]}</dt>
<dd className="grid grid-cols-[minmax(0,1fr)_4.5rem] items-center gap-3">
<span aria-hidden="true" className="flex h-3">
<span
data-slot="comparison-bar"
data-side={side}
className={cn("h-full min-w-1 rounded-xs", SWATCH[side])}
style={{ width: `${length[side] * 100}%` }}
/>
</span>
<span className="text-right text-sm tabular-nums">{measure[side].text}</span>
</dd>
</div>
))}
<div className="col-start-2 row-span-2 row-start-1 justify-self-end">
<dt className="sr-only">{CHANGE}</dt>
<dd>
<MetricDelta value={change(measure)} positiveIsGood={!measure.lowerIsBetter} variant="pill" />
</dd>
</div>
</dl>
</li>
)
}
/**
* Before Northwind against after it: four measures of a finance team's
* reporting, each a pair of bars on its own scale — the old figure in the
* first chart slot, the new in the second — with the figures at their ends and
* the change in the kit's MetricDelta pill, which reads a fall as the win
* where down is the win. Every length and change is worked out from the two
* figures, so the picture cannot disagree with them.
*
* The rows run under a key from `lg`, the name beside the bars; on a phone
* the name sits over them. A server component; every word and figure is in
* `./stats-06.data`.
*/
export function Stats06() {
const titleId = React.useId()
return (
<section
data-section="stats-06"
data-slot="section"
aria-labelledby={titleId}
className="w-full bg-background px-4 py-16 text-foreground md:px-6 md:py-24"
>
<div className="mx-auto max-w-7xl">
<div className="max-w-3xl">
<p className="type-eyebrow">{EYEBROW}</p>
<h2 id={titleId} className="type-display mt-4 text-3xl text-balance md:text-4xl">
{HEADLINE}
</h2>
</div>
{/* The key, for the eye: each row says which side is which in words. */}
<p aria-hidden="true" className="mt-10 flex flex-wrap items-center gap-x-6 gap-y-2 text-sm text-muted-foreground md:mt-12 lg:justify-end">
{(["before", "after"] as const).map((side) => (
<span key={side} className="flex items-center gap-2">
<span className={cn("size-2.5 rounded-xs", SWATCH[side])} />
{SIDES[side]}
</span>
))}
</p>
<ul className="mt-4 border-b border-rule">
{MEASURES.map((measure) => (
<MeasureRow key={measure.label} measure={measure} />
))}
</ul>
<p className="mt-6 max-w-3xl text-sm text-pretty text-muted-foreground tabular-nums">{SOURCE}</p>
</div>
</section>
)
}
export default Stats06/**
* Every word and figure `stats-06` sets: four measures of a finance team's
* reporting, before Northwind and six months after it, each as it reads and
* as it counts. The change on each row is worked out from the two values by
* `change()`, and each bar's length from the larger of its row's two, so the
* picture can never disagree with the figures. Replace the measures with ones
* your customers timed, and say who and when in `SOURCE`.
*/
export const EYEBROW = "Before and after"
export const HEADLINE = "What a finance team's reporting looks like six months after the switch"
export const SOURCE =
"312 customers timed their reporting in the month before they switched, and again in their sixth month on Northwind. Medians."
/** The two sides of every row, as the key above them and the terms of each row's list. */
export const SIDES = { before: "Before Northwind", after: "Six months on" } as const
/** The term for each row's change. */
export const CHANGE = "Change"
/** A figure as it reads and as it counts: `{ value: 8.2, text: "8.2 days" }`. */
export type Figure = { value: number; text: string }
export type Measure = {
label: string
before: Figure
after: Figure
/** True where going down is the win: days to close, hours spent, exports. */
lowerIsBetter: boolean
}
export const MEASURES: readonly Measure[] = [
{ label: "Days to close the month", before: { value: 8.2, text: "8.2 days" }, after: { value: 4.1, text: "4.1 days" }, lowerIsBetter: true },
{ label: "Hours a week building reports", before: { value: 11.5, text: "11.5 h" }, after: { value: 3.2, text: "3.2 h" }, lowerIsBetter: true },
{ label: "Spreadsheet exports a week", before: { value: 26, text: "26" }, after: { value: 18, text: "18" }, lowerIsBetter: true },
{ label: "People who read the Monday review", before: { value: 4, text: "4" }, after: { value: 11, text: "11" }, lowerIsBetter: false },
]
/** The change from before to after, as a ratio: 8.2 days to 4.1 is −0.5. */
export function change(measure: Measure): number {
return measure.before.value ? (measure.after.value - measure.before.value) / measure.before.value : 0
}
/** A bar's length as a share of its row: each side against the larger of the two. */
export function lengths(measure: Measure): { before: number; after: number } {
const longest = Math.max(measure.before.value, measure.after.value)
return longest ? { before: measure.before.value / longest, after: measure.after.value / longest } : { before: 0, after: 0 }
}npx shadcn@latest add @vibra/stats-06Figures that count up
stats-07The page's one dark panel: the claim and where the counts come from beside four figures that count up from zero the first time they come into view, landing on the exact value, and never for a reader who asked for less motion.
import * as React from "react"
import { CountUpFigures } from "./stats-07.client"
import { COUNTERS, EYEBROW, HEADLINE, LEDE } from "./stats-07.data"
/**
* The page's one dark panel, with figures that count: the claim and where the
* counts come from beside four figures, two by two, that count up from zero
* the first time they come into view — and not at all for a reader who asked
* for less motion, or who could already see them when the page loaded.
*
* The panel is inverse by token, not by colour: it carries the `dark` class,
* so every token inside it resolves to the palette's own dark values, a
* preset's included. On a light page it is the dark plane; on a dark page it
* is the card plane, a step off the page and ringed. A server component; the
* figures are the one island, and every word and figure is in
* `./stats-07.data`.
*/
export function Stats07() {
const titleId = React.useId()
return (
<section
data-section="stats-07"
data-slot="section"
aria-labelledby={titleId}
className="w-full bg-background px-4 py-16 text-foreground md:px-6 md:py-24"
>
<div className="mx-auto max-w-7xl">
<div
data-slot="stats-panel"
className="dark grid gap-10 rounded-2xl panel p-6 text-foreground sm:p-10 lg:grid-cols-12 lg:gap-16 lg:p-14"
>
<div className="lg:col-span-5">
<p className="type-eyebrow">{EYEBROW}</p>
<h2 id={titleId} className="type-display mt-4 text-3xl text-balance md:text-4xl">
{HEADLINE}
</h2>
<p className="mt-5 text-base/7 text-pretty text-muted-foreground">{LEDE}</p>
</div>
<CountUpFigures counters={COUNTERS} className="lg:col-span-7" />
</div>
</div>
</section>
)
}
export default Stats07"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
import { prefersReducedMotion } from "@/hooks/use-reduced-motion"
import { formatNumber } from "@/lib/format"
import type { Counter } from "./stats-07.data"
/** How long a count takes, in milliseconds. */
const DURATION = 1400
/** How much of the figures has to be in view before they count. */
const IN_VIEW = 0.35
const easeOutCubic = (t: number) => 1 - (1 - t) ** 3
/** A counter as printed at `progress` of its count, 0 to 1: at 1 it is the value exactly, never an eased approximation. */
function printAt(counter: Counter, progress: number): string {
const value = progress >= 1 ? counter.value : counter.value * easeOutCubic(progress)
const digits = counter.decimals
return `${counter.prefix ?? ""}${formatNumber(value, { minimumFractionDigits: digits, maximumFractionDigits: digits })}${counter.suffix ?? ""}`
}
/**
* The figures, counting up from zero the first time they come into view.
*
* The finished figures are what the server renders and what every reader gets
* first, so nothing is wrong before the script runs, and a reader who asked
* for less motion — by the system setting or `[data-motion="reduced"]` — keeps
* them and never sees a count. For everyone else the island watches the list:
* figures already on screen when the page wakes up have been seen, so they
* stay; figures out of sight go back to zero, and count up once a third of the
* list is in view, landing on the exact value. A screen reader is told the
* finished figure from the start and never hears the numbers go by; the
* finished figure also holds each one's width, so nothing moves while it
* counts, and it is what prints.
*/
export function CountUpFigures({ counters, className }: { counters: readonly Counter[]; className?: string }) {
const list = React.useRef<HTMLDListElement>(null)
// 1 is the finished figures.
const [progress, setProgress] = React.useState(1)
React.useEffect(() => {
const node = list.current
if (!node || typeof IntersectionObserver !== "function" || prefersReducedMotion(node)) return
let frame = 0
let first = true
const observer = new IntersectionObserver(
(entries) => {
const entry = entries[entries.length - 1]
if (first) {
first = false
if (entry.isIntersecting) observer.disconnect()
else setProgress(0)
return
}
if (!entry.isIntersecting || entry.intersectionRatio < IN_VIEW) return
observer.disconnect()
// Asked for less motion since the figures went back to zero: show them finished, uncounted.
if (prefersReducedMotion(node)) {
setProgress(1)
return
}
const start = performance.now()
const tick = (now: number) => {
const next = Math.min(1, Math.max(0, now - start) / DURATION)
setProgress(next)
if (next < 1) frame = requestAnimationFrame(tick)
}
frame = requestAnimationFrame(tick)
},
{ threshold: [0, IN_VIEW] }
)
observer.observe(node)
return () => {
observer.disconnect()
cancelAnimationFrame(frame)
}
}, [])
return (
<dl ref={list} className={cn("grid grid-cols-1 gap-x-10 sm:grid-cols-2", className)}>
{counters.map((counter) => (
<div key={counter.label} className="flex flex-col border-t border-border py-6">
<dt className="order-2 mt-2 text-sm text-pretty text-muted-foreground">{counter.label}</dt>
<dd className="order-1 type-numeral text-4xl md:text-5xl">
<span aria-hidden="true" className="inline-grid">
<span data-slot="count-up-final" className="invisible col-start-1 row-start-1 print:visible">
{printAt(counter, 1)}
</span>
<span data-slot="count-up-value" className="col-start-1 row-start-1 print:hidden">
{printAt(counter, progress)}
</span>
</span>
<span className="sr-only">{counter.spoken}</span>
</dd>
</div>
))}
</dl>
)
}/**
* Every word and figure `stats-07` sets: four counts taken from the product,
* each a value with the decimals it is printed to, what goes before and after
* it, what it counts, and how a screen reader should say it. The island counts
* each one up from zero to exactly its value, so replace the values and the
* figures count to yours.
*/
export const EYEBROW = "Counted from the product"
export const HEADLINE = "What Northwind counted last month"
export const LEDE = "Taken from the product itself at 06:00\u00a0UTC on 1\u00a0September 2026, not estimated and not rounded up."
export type Counter = {
value: number
/** Decimal places, fixed, so the figure keeps its width while it counts: 1 prints 4.1, 2 prints 99.98. */
decimals: number
prefix?: string
suffix?: string
/** The finished figure as a screen reader should say it: "4.1 billion". */
spoken: string
label: string
}
export const COUNTERS: readonly Counter[] = [
{ value: 38_200, decimals: 0, spoken: "38,200", label: "Reviews sent on the last Monday of August" },
{ value: 4.1, decimals: 1, suffix: "B", spoken: "4.1 billion", label: "Events counted in August" },
{ value: 412_000, decimals: 0, spoken: "412,000", label: "Questions answered in plain language" },
{ value: 99.98, decimals: 2, suffix: "%", spoken: "99.98%", label: "Uptime over the last 90 days" },
]npx shadcn@latest add @vibra/stats-07Figures by company size
stats-08Three company sizes as a row of chips, and under them the same four medians for the size chosen, each size saying how many companies its figures come from.
import * as React from "react"
import { formatNumber } from "@/lib/format"
import { SegmentTabs } from "./stats-08.client"
import { ALL_SIZES, BASIS, EYEBROW, HEADLINE, LEDE, MEASURES, SEGMENTS, SEGMENTS_LABEL, type Segment } from "./stats-08.data"
/**
* One segment's figures: the same four measures in the same order for every
* size, set large over what each measures and the same median across every
* size, then the size and how many companies the medians come from. Rules,
* not boxes.
*/
function SegmentFigures({ segment }: { segment: Segment }) {
const basis = BASIS.replace("{size}", segment.size).replace("{count}", formatNumber(segment.companies))
return (
<div>
<dl className="grid grid-cols-2 gap-x-6 gap-y-8 lg:grid-cols-4 lg:gap-x-0 lg:border-t lg:border-rule lg:pt-8">
{MEASURES.map((measure, index) => (
<div
key={measure.label}
className="flex flex-col border-t border-rule pt-6 lg:border-t-0 lg:border-l lg:px-8 lg:pt-0 lg:first:border-l-0 lg:first:pl-0 lg:last:pr-0"
>
<dt className="order-2 mt-2 text-sm font-medium text-pretty">{measure.label}</dt>
<dd data-slot="segment-figure" className="order-1 type-numeral text-4xl sm:text-5xl">
{segment.figures[index]}
</dd>
<dd className="order-3 mt-1 text-sm text-muted-foreground tabular-nums">{ALL_SIZES.replace("{value}", measure.all)}</dd>
</div>
))}
</dl>
<p className="mt-8 text-sm text-pretty text-muted-foreground tabular-nums">{basis}</p>
</div>
)
}
/**
* The figures for a team the reader's size: three company sizes as a row of
* chips, and under them the same four medians for the size chosen, so a
* startup is not sold on an enterprise's numbers or the other way round. Each
* size says how many companies its medians come from, and together they are
* every company on a paid plan.
*
* The chips are the one island, the kit's Tabs; the figures for every size
* are drawn here and handed to it. Two figures to a row on a phone, four
* from `lg`. A server component; every word and figure is in
* `./stats-08.data`.
*/
export function Stats08() {
const titleId = React.useId()
const tabs = SEGMENTS.map((segment) => ({
value: segment.id,
label: segment.label,
panel: <SegmentFigures segment={segment} />,
}))
return (
<section
data-section="stats-08"
data-slot="section"
aria-labelledby={titleId}
className="w-full bg-background px-4 py-16 text-foreground md:px-6 md:py-24"
>
<div className="mx-auto max-w-7xl">
<div className="mb-10 max-w-2xl md:mb-12">
<p className="type-eyebrow">{EYEBROW}</p>
<h2 id={titleId} className="type-display mt-4 text-3xl text-balance md:text-4xl">
{HEADLINE}
</h2>
<p className="mt-5 text-base/7 text-pretty text-muted-foreground md:text-lg/8">{LEDE}</p>
</div>
<SegmentTabs tabs={tabs} label={SEGMENTS_LABEL} />
</div>
</section>
)
}
export default Stats08"use client"
import * as React from "react"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
/** One segment as the island receives it: its chip, and its figures drawn by the server file. */
export type SegmentTab = { value: string; label: string; panel: React.ReactNode }
/**
* The segment toggle is the kit's Tabs — one choice drives one set of figures
* — so the arrow keys move between sizes and bring each one's figures up as
* they land, Home and End jump to the ends, and a reader hears which size is
* chosen. The chosen size is a chip on the accent's tint at the 500 weight,
* never a stroke; the others carry the hairline so they read as chips before
* anyone hovers. Every figure arrives drawn, as plain props, from the section.
*/
export function SegmentTabs({ tabs, label }: { tabs: readonly SegmentTab[]; label: string }) {
return (
<Tabs defaultValue={tabs[0]?.value} className="gap-0">
<TabsList
variant="line"
aria-label={label}
activateOnFocus
className="w-full flex-wrap justify-start gap-2 p-0 group-data-horizontal/tabs:h-auto"
>
{tabs.map((tab) => (
<TabsTrigger
key={tab.value}
value={tab.value}
className="h-8 flex-none rounded-full px-3.5 font-normal ring-1 ring-border after:hidden hover:bg-muted/50 data-active:bg-brand-muted data-active:font-medium data-active:ring-transparent data-active:hover:bg-brand-muted"
>
{tab.label}
</TabsTrigger>
))}
</TabsList>
{tabs.map((tab) => (
<TabsContent key={tab.value} value={tab.value} className="mt-10 rounded-xl focus-ring md:mt-12">
{tab.panel}
</TabsContent>
))}
</Tabs>
)
}/**
* Every word and figure `stats-08` sets: the same four medians for companies
* of three sizes, so a reader can choose theirs, each beside the median across
* every size. Every segment gives the same measures in the same order, so
* choosing another only changes the figures; the companies behind the
* segments add up to all 2,400, which `total()` counts. Replace the segments
* with your own cut and its medians.
*/
export const EYEBROW = "By company size"
export const HEADLINE = "The numbers for a team your size"
export const LEDE =
"Medians for workspaces created between January and June 2026, split by the headcount of the company behind them. Choose the size closest to yours."
/** The segment control's name, for a screen reader. */
export const SEGMENTS_LABEL = "Company size"
/** Under each segment's figures: `{size}` and `{count}` are its headcount and how many companies it is. */
export const BASIS = "{size}, {count} companies on a paid plan."
/** Under each figure: `{value}` is the same measure across every size. */
export const ALL_SIZES = "All sizes: {value}"
/** What every segment measures, in the order its figures are drawn, and the median across every size. */
export const MEASURES = [
{ label: "From sign-up to the first report", all: "11 min" },
{ label: "Sources connected", all: "3.4" },
{ label: "Back each week from reporting", all: "8 h" },
{ label: "People in the Monday review", all: "6" },
] as const
export type Segment = {
id: string
label: string
/** The headcount the segment covers, said under its figures. */
size: string
companies: number
/** One figure per measure, in `MEASURES` order. */
figures: readonly [string, string, string, string]
}
export const SEGMENTS: readonly Segment[] = [
{ id: "startup", label: "Startups", size: "Under 50 people", companies: 1_120, figures: ["8 min", "2.8", "5.5 h", "3"] },
{ id: "midmarket", label: "Mid-market", size: "50 to 500 people", companies: 960, figures: ["12 min", "3.9", "9.5 h", "7"] },
{ id: "enterprise", label: "Enterprise", size: "Over 500 people", companies: 320, figures: ["26 min", "6.2", "21 h", "14"] },
]
/** How many companies the segments cover together. */
export function total(segments: readonly Segment[] = SEGMENTS): number {
return segments.reduce((sum, segment) => sum + segment.companies, 0)
}npx shadcn@latest add @vibra/stats-08