Stat
The KPI / metric tile a dashboard opens with — a labelled number with an optional sentiment-aware delta and a chart-agnostic trend slot.
@garn/ui/statOn this page
Default
The terse one-liner — label, compact value, percent delta.
Carded
Composed onto Card with an icon, currency value, and description.
Delta Sentiment
Same numbers, different meaning — sentiment="inverse" colours an up-is-bad metric red.
Delta Appearance
text (inline coloured arrow) vs soft (a tinted lozenge).
Formats
currency · compact · percent · suffix unit · a raw node value.
Sizes
sm · md · lg.
Trend
A token-styled sparkline dropped into the chart-agnostic Trend slot.
Loading
The loading preset holds the footprint with Skeleton blocks.
Group
Stat.Group — a four-column responsive row of KPIs.
Group Divided
Stat.Group with seam separators inside a Card.
Group Cards
A row of individually carded tiles (auto-fit by minTileWidth).
Composed
Full control via slots — Stat.Icon / .Label / .Value / .Delta / .Description.
Link
asChild turns the whole tile into a drill-down link with a focus ring.
Installation
garn is copy-in — the garn CLI writes this component's source into your project, so you own and can edit it. It resolves any base files and installs the npm dependencies below for you.
npx garn-ui add stat- Registry
stat- Deps
class-variance-authority@radix-ui/react-slotlucide-react- Registry deps
utilsskeleton
Import
import { Stat.Icon, Stat.Label, Stat.Value, Stat.Delta, Stat.Description, Stat.Trend, Stat.Group } from "@garn/ui/stat";Anatomy
The parts this component is built from — compose them to assemble it.
Stat.Icon[data-slot="stat-icon"]Leading glyph (decorative, aria-hidden). Sizes with the tile.
Stat.Label[data-slot="stat-label"]The metric's name. Registers itself so the value can aria-describedby it; truncates.
Stat.Value[data-slot="stat-value"]The headline number — a numeric child is Intl-formatted; any node renders verbatim. tabular-nums. Hosts stat-value-prefix / stat-value-suffix.
Stat.Delta[data-slot="stat-delta"]The change indicator — tone follows good/bad (direction × sentiment), shows a directional arrow, and announces the direction in words (sr-only). Hosts an optional stat-delta-trailing.
Stat.Description[data-slot="stat-description"]Supporting copy under the value.
Stat.Trend[data-slot="stat-trend"]A bare, presentational slot for a trend visual (sparkline / progress / mini bar). Stat ships no charting dep — it only reserves the box for the future Chart recipe.
Stat.Group[data-slot="stat-group"]A responsive role=group grid of tiles. Props: columns (fixed tracks) or minTileWidth (auto-fit), divided (seam separators), size (ambient, inherited by child Stats), label (accessible name). Hosts stat-group-label.
Props
The component's public props and their types.
align- Description
- No description.
- Type
"center" | "end" | "start"- Default
start
size- Description
- No description.
- Type
"lg" | "md" | "sm"- Default
md
asChild- Description
- Render as the child element (a link/button drill-down tile).
- Type
boolean- Default
false
delta- Description
- Terse: the change. Number infers direction from its sign.
- Type
React.ReactNode
deltaAppearance- Description
- Delta presentation — inline coloured arrow, or a tinted lozenge.
- Type
StatDeltaAppearance- Default
"text"
deltaFormat- Description
- How the terse `delta` is formatted (the KPI convention is a percent).
- Type
StatNumberFormat- Default
"percent"
deltaPrecision- Description
- Fraction digits for the terse `delta`.
- Type
number
description- Description
- Terse: supporting copy under the value.
- Type
React.ReactNode
direction- Description
- Override the delta's inferred direction.
- Type
StatDirection
icon- Description
- Terse: leading icon (decorative).
- Type
React.ReactNode
label- Description
- Terse: the metric's name.
- Type
React.ReactNode
loading- Description
- Swap the value/label/delta for sized Skeleton blocks; keeps the footprint.
- Type
boolean- Default
false
prefix- Description
- Rendered before the value.
- Type
React.ReactNode
sentiment- Description
- What "good" means for the delta. Default `positive`.
- Type
StatSentiment
suffix- Description
- Rendered after the value.
- Type
React.ReactNode
trend- Description
- Terse: a trend visual slot (sparkline / progress) at the foot.
- Type
React.ReactNode
value- Description
- Terse: the headline value (number → Intl-formatted, or any node).
- Type
React.ReactNode
Plus 282 inherited native <div> attributes.
Stat.Value
children- Description
- A number (formatted via Intl) or any node (rendered verbatim, e.g. "—").
- Type
React.ReactNode
prefix- Description
- Rendered before the value (a unit, "$").
- Type
React.ReactNode
suffix- Description
- Rendered after the value (a unit, "ms", "/mo").
- Type
React.ReactNode
Plus 282 inherited native HTML attributes.
Stat.Delta
appearance- Description
- `text` (coloured inline arrow) or `soft` (a tinted lozenge).
- Type
StatDeltaAppearance- Default
"text"
children- Description
- The change. A number formats via Intl + infers direction from its sign.
- Type
React.ReactNode
direction- Description
- Override the inferred direction (defaults to the sign of a numeric child).
- Type
StatDirection
hideIcon- Description
- Hide the directional arrow glyph.
- Type
boolean- Default
false
label- Description
- Accessible phrasing of the change, when the value alone isn't self-describing.
- Type
string
sentiment- Description
- What "good" means — flips which direction reads green.
- Type
StatSentiment- Default
"positive"
trailing- Description
- Trailing context, rendered muted ("vs. last week").
- Type
React.ReactNode
Plus 282 inherited native HTML attributes.
Stat.Group
columns- Description
- Number of columns. Default: auto-fit by min tile width.
- Type
number
divided- Description
- Seam separators between tiles (the divided stat-row look).
- Type
boolean
label- Description
- Accessible name for the group of metrics.
- Type
React.ReactNode
minTileWidth- Description
- Min tile width for the auto-fit grid (when `columns` is unset). Default 220.
- Type
number
size- Description
- Ambient size inherited by child Stats (each may override). Default `md`.
- Type
StatSize
Plus 280 inherited native HTML attributes.
Styling
Target these data-slots and states, and remap these tokens, to restyle without forking the component.
[data-slot="stat"]States
| Selector | State | Description |
|---|---|---|
[data-loading] | loading | `loading` swaps the value/label/delta for sized Skeleton blocks and sets aria-busy; the tile holds its footprint. |
[data-slot=stat-delta][data-direction] | delta-direction | The delta reflects data-direction (up | down | neutral) and the resolved data-tone (positive | negative | muted) for styling hooks. |
| Property | Token | Tier |
|---|---|---|
gap | --garn-gap-stack | semantic · air |
deltaPositive | --garn-success | semantic |
deltaNegative | --garn-danger | semantic |
Private instance vars (never externalize): --d-fg
When to use
A headline metric on a dashboard / overview / report — a labelled number with an optional period-over-period delta, an icon, supporting copy, and a trend slot. Use Stat.Group for a responsive row of related KPIs.
Reach for something else when
- An inline numeric token next to a label (use Counter or Badge)
- a single progress bar toward a goal (use Progress)
- a generic surface or content panel (use Card — Stat composes onto it)
- tabular numbers in rows and columns (use Table).
Overview
Stat is a headline metric for a dashboard, overview, or report — a labelled number with an optional period-over-period delta, an icon, supporting copy, and a trend slot. Stat.Value Intl-formats a numeric child (tabular-nums), Stat.Label names it (and the value aria-describedbys it), and Stat.Delta colors itself by good/bad with a directional arrow announced in words. Stat.Group lays out a responsive row of related KPIs. It's for a single key figure — for many rows of records use a Table.
Guidelines
Show the few numbers that matter, with context. A KPI tile earns attention only if it's genuinely headline; a wall of stats dilutes them all. A bare number means little — pair it with a label and, where relevant, a delta or comparison period.
Make the delta's meaning unambiguous. Up isn't always good (churn, cost), so let the tone follow good/bad sentiment rather than direction alone, and state the comparison ("vs last week"). The arrow and sr-only wording keep it accessible.
Keep figures aligned and formatted. Lean on the Intl formatting and tabular numerals so digits line up across a Stat.Group, use consistent units/precision, and let Stat.Description carry caveats instead of cramming them into the value.
Best practices
- Name every metric (label / Stat.Label / aria-label) — the value alone is unscannable.
- Use
sentiment="inverse"for metrics where up is bad (churn, latency, cost, error rate) so the delta colours correctly. - Wrap a Stat in Card when you want a surface — Stat stays surface-less so it also lives in a cell / list / popover.
- Format numbers in-component (format / currency / precision / locale) rather than pre-stringifying, so tabular-nums and locale stay consistent.
- Use
loadingto hold the tile's footprint while data resolves.
- Don't encode good/bad with a raw colour prop — there is none by design; let direction × sentiment resolve the tone.
- Don't bake a surface into the Stat — compose Card around it.
- Don't put a chart library inside Stat.Value; drop the visual into Stat.Trend (the reserved, chart-agnostic slot).
- Don't strip the sr-only direction phrase or the focus ring on an asChild tile.
Content guidelines
- Keep labels short and scannable; let the value be the visual anchor.
- Phrase the delta's trailing context ('vs. last week') as comparison, not a second metric.
Troubleshooting
An up-is-bad metric (churn / latency / cost) shows green when it rises.
Cause. Direction maps to tone through sentiment, which defaults to positive (up = good).
Fix. Pass sentiment="inverse" so a rise reads red and a fall reads green.
The terse delta prints as a tiny fraction or the wrong unit.
Cause. The delta defaults to percent formatting (the KPI convention), independent of the value's format.
Fix. Pass an absolute number for percent (0.125 → 12.5%), or set deltaFormat="standard" / "currency" for a non-percent change.
The tile has no border / background.
Cause. Stat is surface-less by design so it composes anywhere.
Fix. Wrap it in a Card (or any surface) — don't add ad-hoc padding/border to the Stat.
A dev console warns the Stat has no name.
Cause. No label, <Stat.Label>, or aria-label was provided.
Fix. Give the metric a name so the value is scannable and announced.
asChild throws or doesn't render the number.
Cause. Slot needs exactly one element child; the label/value/delta are injected into it.
Fix. Pass a single element (e.g. an <a>) as the child; keep terse props on the Stat for the injected stack.
Accessibility
- Role
group- Focus
- No focus of its own unless asChild makes it a link/button, which keeps a 2px --garn-ring focus ring.
Accessibility requirements
Give the metric a name via `label`, a <Stat.Label>, or `aria-label` so the value is scannable and announced.
when a Stat has no `label`, no <Stat.Label>, and no `aria-label` (and isn't loading)
- The tile is a role=group named by its label (or aria-label); the value is aria-describedby its label so screen readers read 'label, value' together.
- The delta's direction reaches AT in words via an sr-only phrase ('up 12.5%') — colour and the arrow glyph are never the only signal (WCAG 1.4.1).
- The icon and the directional arrow are decorative (aria-hidden); the trend slot is role=presentation.
- asChild turns the whole tile into a focusable drill-down link/button — keep the focus ring; the label/value/delta are injected as that element's children.