Stat
A KPI/metric card for dashboards: a label, a big tabular-nums value, a directional trend chip, and optional icon or sparkline. Compose the named parts into a tile, then drop a row of them above a Data Table.
Installation#
Usage#
Stat is composed from named parts, like Card. The Stat root owns the surface, variant, and density; the parts (StatLabel, StatValue, StatTrend) read it from context.
import {
Stat,
StatHeader,
StatLabel,
StatValue,
StatTrend,
StatCaption,
StatFooter,
StatIcon,
} from "@/components/ui/stat"
import { ShoppingBag } from "@phosphor-icons/react"
export function OrdersStat() {
return (
<Stat>
<StatHeader>
<StatLabel>Total orders</StatLabel>
<StatIcon className="bg-primary/10 text-primary"><ShoppingBag /></StatIcon>
</StatHeader>
<StatValue>1,429</StatValue>
<StatFooter>
<StatTrend direction="up">12.4%</StatTrend>
<StatCaption>vs last month</StatCaption>
</StatFooter>
</Stat>
)
}Group#
StatGroup fuses a row of tiles into one segmented surface, the way a dashboard bands its headline KPIs together instead of floating them as separate cards. The group owns the single border, radius, and shadow; each nested Stat senses the group and sheds its own chrome to lie flush, with hairline dividers between. Set the widest-breakpoint column count with columns (2 | 3 | 4); it steps down gracefully on narrow screens.
Anatomy#
A tile reads top-to-bottom: a StatHeader pairs the label with a tinted StatIcon (or an action), the StatValue carries the figure in tabular-nums so it never reflows, and a StatFooter sets the trend beside a muted comparison caption.
Trend#
StatTrend is a soft chip whose arrow and color come from direction: up reads green, down red, and neutral muted. For metrics where down is the good outcome (refunds, churn, bounce rate), set inverted: the color flips to match the meaning while the arrow keeps pointing the honest way. The chip never wraps or shrinks, so a “+1.1 pts” in a narrow tile stays one chip and the row beside it gives way.
Sparkline#
StatSparkline is built on Chart in sparkline mode: a soft area fill under a trend line that bleeds to the bottom and side edges of the tile. It paints in currentColor, so set the hue with a text utility (text-success, text-destructive) and it themes across all four palettes for free. Pass tooltip to light up each point on hover with a gliding bubble.
Count up#
Pass countUp to StatValue and the figure counts up from zero to its value the first time it scrolls into view, the same trigger the marketing sections use, re-counting each time it scrolls back in. It climbs fast and decelerates onto the final figure, so it reads as a real count-up. It reads the string children ("4,100+", "$4.2M", "4.6×") and preserves the prefix, suffix, and comma/decimal format as it counts, riding the built-in tabular-nums so the row never jitters as the count climbs. It no-ops for number-less content and honors prefers-reduced-motion by showing the final figure immediately.
Variants#
default sits on a hairline border with a soft shadow; outline drops the shadow for flat, gridded dashboards; elevated trades the border for a lifted shadow when a tile needs to float; plain has no tile at all.
Without a frame#
Most dashboards lead a chart or a card with one big figure, and that figure is not a tile. variant="plain" drops the ring, the shadow, the fill and the padding, and keeps everything else: the display-face figure, the density’s type ramp, and a tighter rhythm between the parts, since there is no frame left to space them against. Lay it on the sheet beside a chart, or inside a Card, where the card is the frame. Inside a StatGroup the group already owns the frame, so a plain tile keeps the group’s padding.
Parts on their own#
The parts do not need a Stat around them. StatValue, StatTrend, StatLabel and StatCaption (and the header, footer and icon rows) render anywhere with the plain look at the ambient density, so the headline figure of a chart card is a StatValue, and the trend chip beside a number in a description is a StatTrend, the same chip a tile carries. countUp works there too. Only StatSparkline needs a Stat, because it bleeds to the tile’s edges.
Density#
Density is Koala’s cross-cutting spacing axis (see Density). For Stat it tunes padding, gap, the icon tile, and the value size. compact is the dashboard default; comfortable is roomier. Set it per-tile or for a whole grid with DensityProvider.
Interactive#
A KPI tile is data, not a control, so it stays inert by default. When the whole tile should drill into a detail view, pass asChild to render it as a link and interactive to add the pointer, hover lift, and focus ring. For a per-tile menu instead, drop a Button into the header.
API reference#
Stat forwards all div props and adds variant (default | outline | elevated | plain), density (compact | comfortable), interactive, and asChild. StatGroup bands a row of tiles into one surface and forwards div props plus variant (default | outline | elevated), columns (2 | 3 | 4), and asChild. The parts StatHeader, StatLabel, StatValue, StatCaption, StatFooter, StatIcon all forward div props; StatValue adds countUp to count a string figure up from zero on scroll. StatTrend adds direction (up | down | neutral) and inverted; StatSparkline takes a data: number[], an optional strokeWidth, and tooltip to enable per-point hover. Every part but StatSparkline also renders outside a Stat, with the plain look at the ambient density. Every part accepts className, merged last.