X Data

Star on GitHubFeedback
X DataNative HTMLot-stat

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 Stat

Example

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.json

Usage

This example also uses Select.

npx shadcn@latest add https://thedirectors.agency/ui/r/select.json
stat-example.tsx
"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>; each Stat is 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 (a ot-fraction with 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

Props of Stat
PropTypeDefaultWhat it does
Statsdl propsThe 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 valuenumber | stringThe 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 labelReactNodeWhat the figure counts: visits in September. Read before the figure.
Stat noteReactNodeA line about it, in pencil: up 12% on August.
Stat format(value: number) => stringen-GB groupingFormats a number value, such as Intl.NumberFormat for pounds.

Craft

Where its move comes from

Where its move comes from
ComponentReferenceMove
StatSPECTRA crop; POINTGiant thin figures cut off by the one hairline under the row
Beside statWOVE's 03; "the silence that heals" 14/08A 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

How it moves
ComponentArticulationWhat moves
StatRollA changed figure turns over like a counter's wheels, units first, only the digits that changed