Stat
A named value: a giant thin figure whose feet sink under one hairline, the name small beneath; a changed figure turns over like a counter's wheels.
Report an issue with StatExample
Crop
- Visits in September
- 4 210
- Projects shipped
- 12
- Fees invoiced
- £304 000
Beside
- Visits in September
- 4 210
- The best month of the year
- Projects shipped
- 12
- Web, identity and motion
- Fees invoiced
- £304 000
- Before tax
Grid
- Visits in September
- 4 210
- The best month of the year
- Projects shipped
- 12
- Web, identity and motion
- Fees invoiced
- £304 000
- Before tax
One stat, a string value
- Invoices paid on time
- 98%
Beside, with a note
- Ambient
- 03
- Already on, and never turns off.
Install
Run it in a project that has the shadcn CLI set up. It brings the 0nlyType base and fraction along.
npx shadcn@latest add https:// thedirectors.agency/ ui/ r/stat.jsonUsage
This example also uses Select.
npx shadcn@latest add https:// thedirectors.agency/ ui/ r/ select.json"use client"import * as React from "react"import { Select } from "@/components/ui/select"import { Stat, Stats, type StatsProps } from "@/components/ui/stat"const years = {"2026": { visits: 4210, projects: 12, fees: 304000, hours: "1,860" },"2025": { visits: 3120, projects: 9, fees: 251000, hours: "1,745" },"2024": { visits: 2380, projects: 11, fees: 198000, hours: "1,902" },}type Year = keyof typeof yearsconst pounds = new Intl.NumberFormat("en-GB", { style: "currency", currency: "GBP", maximumFractionDigits: 0 })function Year({ year, variant }: { year: Year; variant?: StatsProps["variant"] }) {const y = years[year]return (<Stats variant={variant} aria-label={`The studio in ${year}`} className="w-full"><Stat value={y.visits} label="Visits in September" note={variant ? "The best month of the year" : undefined} /><Stat value={y.projects} label="Projects shipped" note={variant ? "Web, identity and motion" : undefined} /><Stat value={y.fees} format={(n) => pounds.format(n).replace(/,/g, "\u202f")} label="Fees invoiced" note={variant ? "Before tax" : undefined} /></Stats>)}export default function Example() {const [year, setYear] = React.useState<Year>("2026")return (<div className="grid w-full grid-cols-[minmax(0,1fr)] justify-items-start gap-12"><Select label="The studio in" value={year} onChange={(e) => setYear(e.target.value as Year)}>{Object.keys(years).map((y) => <option key={y}>{y}</option>)}</Select><div className="grid w-full gap-3"><span className="ot-label">Crop</span><Year year={year} /></div><div className="grid w-full gap-3"><span className="ot-label">Beside</span><Year year={year} variant="beside" /></div><div className="grid w-full gap-3"><span className="ot-label">Grid</span><Year year={year} variant="grid" /></div></div>)}Contract
- Underneath: native
<dl>; eachStatis a<div>of<dt>(what it counts) and<dd>s (the figure, and a note). - Anatomy:
<dl class="ot-stats" data-variant="beside | grid" style="--row-len">of<div class="ot-stat" style="--len">holding.ot-stat-label,.ot-stat-figure(aot-fractionwith no total, set in the stat's own face: roman, thin, the cell's size; its figures turn on their own wheels and it is read whole) and.ot-stat-note. - crop (the default): SPECTRA's giant thin figures (weight 200), set as large as the cell allows; one hairline runs under the whole row and cuts off their feet (text-box trimmed to the cap height), the name small beneath. A row shares one size, set by its longest figure (
--row-len), so the figures stand on one line. Numbers are grouped by a thin space, as SI sets them, since the crop would cut a comma's tail. - beside: WOVE's 03 and the "14/08" of "the silence that heals": the figure heavy (700) and whole, its name and a line about it set beside it, their caps on its cap line. The stats stand in a list, figures flush to one edge in tabular figures so the units line up, the words in one column.
- grid: "Less is more.": each stat a cell of a hairline grid, the lines between cells only; the name in the cell's first corner, the figure in the far one.
- A changed value turns over like a counter's wheels (the Fraction's): only the digits that changed turn, the units first and each carry one arpeggio behind, down for a smaller value and up for a larger; a figure that gains or loses a place turns over whole. Nothing moves until the value does.
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
Stats | dl props | The row the stats stand in; they wrap to the width, one per line on a phone. A single stat still goes in Stats, since each is a term and its value. | |
Stats variant | "crop" | "beside" | "grid" | "crop" | crop: SPECTRA's giant thin figures, each set as large as its cell allows, their feet cut off by one hairline that runs under the whole row, the name small under it. beside: WOVE's 03, the figure heavy and whole with its name and a line about it set beside it, on its cap line. grid: "Less is more.", each stat a cell of a hairline grid, its name in the first corner and its figure in the far one; the lines run only between cells. |
Stat value | number | string | The figure. A number is formatted; a string ("12:40", "98%") is set as it is. When it changes, only the figures that changed turn over, units first, the way the value went. | |
Stat label | ReactNode | What the figure counts: visits in September. Read before the figure. | |
Stat note | ReactNode | A line about it, in pencil: up 12% on August. | |
Stat format | (value: number) => string | en-GB grouping | Formats a number value, such as Intl.NumberFormat for pounds. |
Craft
Where its move comes from
| Component | Reference | Move |
|---|---|---|
| Stat | SPECTRA crop; POINT | Giant thin figures cut off by the one hairline under the row |
| Beside stat | WOVE's 03; "the silence that heals" 14/08 | A heavy figure with its name and a line set beside it |
| Grid stat | "Less is more." | A hairline grid, the name in one corner and the figure in the far one |
How it moves
| Component | Articulation | What moves |
|---|---|---|
| Stat | Roll | A changed figure turns over like a counter's wheels, units first, only the digits that changed |