Stat
The KPI / metric tile a dashboard opens with — a labelled number with an optional sentiment-aware delta, an icon, a description, and a chart-agnostic trend slot. Terse one-liner or full slot composition; Stat.Group lays a responsive row of tiles.
@garn/ui/statDefault
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- Source
- stat.tsx
- Deps
class-variance-authority@radix-ui/react-slotlucide-react- Registry deps
utilsskeleton
Import
import { Stat } from "@garn/ui/stat";Props
Generated from the component's TypeScript 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
delta- Description
- Terse: the change. Number infers direction from its sign.
- Type
React.ReactNode
deltaAppearance- Description
- Delta presentation — `text` (default) or `soft` lozenge.
- Type
StatDeltaAppearance
deltaFormat- Description
- How the terse `delta` is formatted. Default `percent` (the KPI convention).
- Type
StatNumberFormat
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
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, default) or `soft` (a tinted lozenge).
- Type
StatDeltaAppearance
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
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. Default `positive`.
- Type
StatSentiment
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 remap these tokens to restyle without forking the component.
[data-slot="stat"]| Property | Token | Tier |
|---|---|---|
gap | --garn-gap-stack | semantic · air |
deltaPositive | --garn-success | semantic |
deltaNegative | --garn-danger | semantic |
Private instance vars (never externalize): --d-fg
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.
Accessibility
- Role
group- Focus
- No focus of its own unless asChild makes it a link/button, which keeps a 2px --garn-ring focus ring.
Enforceable contracts
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.
- Status
- Stable
- Version
v0.3.0- Introduced
v0.1.0
garn doesn't yet record a per-release version history. This reflects the component's current lifecycle state; full changelog entries will appear once the metadata pipeline emits a lifecycle.history field.