Skip to content

Stat

The KPI / metric tile a dashboard opens with — a labelled number with an optional sentiment-aware delta and a chart-agnostic trend slot.

Stablev0.4.2added in v0.1.0@garn/ui/stat
On 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.

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.

terminal
npx garn-ui add stat
First time? Set up garn in your project
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

SelectorState
[data-loading]loading
[data-slot=stat-delta][data-direction]delta-direction
PropertyTokenTier
gap--garn-gap-stacksemantic · air
deltaPositive--garn-successsemantic
deltaNegative--garn-dangersemantic

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

Do
  • 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 loading to hold the tile's footprint while data resolves.
Don't
  • 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

warn

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)

A1.3.1Info and RelationshipsA1.4.1Use of ColorAA1.4.3Contrast (Minimum)A4.1.2Name, Role, Value
  • 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.