StatCard

One metric, its change, and an optional sparkline.

Projects published
+12%
1,284

Usage

import { StatCard } from "@voila.dev/ui/stat-card";
<StatCard.Root>
	<StatCard.Header>
		<StatCard.Label>Projects published</StatCard.Label>
		<StatCard.Delta trend="up">+12%</StatCard.Delta>
	</StatCard.Header>
	<StatCard.Value>1,284</StatCard.Value>
</StatCard.Root>

The label and the delta share the header row; the value sits under them on its own line, so a row of cards lines its numbers up whether or not each one has a delta to show.

trend picks the arrow and the colour; it is separate from the text because a falling number is not always bad — cancellations going down is a down trend you would want to read as good.

Recipes

A sparkline for the shape of the number

Projects published
+12%
1,284
Chart of Projects published over 6 points
monthProjects published
January24
February31
March28
April35
May42
June38

StatCard.Chart is a full-bleed slot pinned to the bottom edge: it cancels the card's bottom padding and the card clips the chart to its rounded corners. Constrain the height on the Chart.Root itself and drop the margins, since a sparkline has no axes to make room for.

<StatCard.Chart>
	<Chart.Root
		config={config}
		data={data}
		x={{ key: "month", type: "point" }}
		y={{ keys: ["projects"] }}
		className="aspect-auto h-16 w-full"
		margin={{ top: 4, right: 0, bottom: 0, left: 0 }}
		interactive
	>
		<Chart.Cursor />
		<Chart.Area />
		<Chart.Tooltip />
	</Chart.Root>
</StatCard.Chart>

The cursor and the tooltip work in a 64-pixel-tall chart exactly as they do in a full-size one, so a sparkline can still be read value by value.

The three sparkline shapes

Projects published
+12%
1,284
Chart of Projects published over 6 points
monthProjects published
January24
February31
March28
April35
May42
June38
Projects published
+12%
1,284
Chart of Projects published over 6 points
monthProjects published
January24
February31
March28
April35
May42
June38
Projects published
+12%
1,284
Chart of Projects published over 6 points
monthProjects published
January24
February31
March28
April35
May42
June38

An area for a volume that accumulates, a line for a rate, bars for counts per period. Bars need a band axis to sit in; a line or an area wants point.

Falling numbers that are good news

Cancellations
-45%
17
Chart of Cancellations over 6 points
monthCancellations
January31
February27
March24
April22
May19
June17
<StatCard.Delta trend="down" className="text-success">
	-45%
</StatCard.Delta>

trend keeps the arrow pointing down while the colour says the metric is moving the right way.

Loading and empty

Awaiting data
New this week
0
No data yet

Chart.Skeleton and Chart.Empty slot straight into StatCard.Chart. Pad the slot back (px-4, plus pb-4 for the empty state) so the placeholder sits inside the card instead of bleeding to its edges like a real chart.

Compact, for dense dashboards

Published
1,284
Chart of Projects published over 6 points
monthProjects published
January24
February31
March28
April35
May42
June38
Booked
1,102
Chart of Projects published over 6 points
monthProjects published
January24
February31
March28
April35
May42
June38
Cancelled
17
Chart of Projects published over 6 points
monthProjects published
January24
February31
March28
April35
May42
June38
Fill rate
92%
Chart of Projects published over 6 points
monthProjects published
January24
February31
March28
April35
May42
June38

size="sm" on the root tightens the padding and the gaps; the anatomy is unchanged.

A KPI strip

Projects published
+12%
1,284
Chart of Projects published over 6 points
monthProjects published
January24
February31
March28
April35
May42
June38
Cancellations
-45%
17
Chart of Cancellations over 6 points
monthCancellations
January31
February27
March24
April22
May19
June17
Bookings confirmed
0%
1,102
Chart of Projects published over 6 points
monthProjects published
January24
February31
March28
April35
May42
June38

API

StatCard.Root

PropTypeDefault
size"default" | "sm"
renderComponentRenderFn<HTMLProps, {}> | ReactElement<unknown, string | JSXElementConstructor<any>>Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a ReactElement or a function that returns the element to render.

Plus the DOM props of the element it renders. Source: stat-card/components/stat-card-root.tsx.

StatCard.Chart

Plus every <div> prop. Source: stat-card/components/stat-card-chart.tsx.

StatCard.Delta

PropTypeDefault
trend"down" | "neutral" | "up""neutral"Direction of the change: up renders success, down destructive, neutral muted. Set it by what the movement means, not by its sign — churn going up is not a success.

Plus every <div> prop. Source: stat-card/components/stat-card-delta.tsx.

StatCard.Header

PropTypeDefault
renderComponentRenderFn<HTMLProps, {}> | ReactElement<unknown, string | JSXElementConstructor<any>>Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a ReactElement or a function that returns the element to render.

Plus every <div> prop. Source: stat-card/components/stat-card-header.tsx.

StatCard.Label

PropTypeDefault
renderComponentRenderFn<HTMLProps, {}> | ReactElement<unknown, string | JSXElementConstructor<any>>Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a ReactElement or a function that returns the element to render.

Plus every <div> prop. Source: stat-card/components/stat-card-label.tsx.

StatCard.Value

PropTypeDefault
renderComponentRenderFn<HTMLProps, {}> | ReactElement<unknown, string | JSXElementConstructor<any>>Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a ReactElement or a function that returns the element to render.

Plus every <div> prop. Source: stat-card/components/stat-card-value.tsx.