Usage
import { StatCard } from "@hoplallc/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
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.
Give the <Chart> a height, hide its axes and drop the
margins, since a sparkline has no axes to make room for.
import { areaY, defineChart } from "@hoplallc/chart";
import { Chart } from "@hoplallc/chart/react";
<StatCard.Chart>
<Chart
ariaLabel="Projects published"
height={64}
legend={false}
definition={defineChart({
x: { axis: false },
y: { axis: false, grid: false },
margin: { top: 4, right: 0, bottom: 0, left: 0 },
marks: [areaY(data, { x: "month", y: "projects", line: true })],
})}
/>
</StatCard.Chart>The cursor, the tooltip and the keyboard 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
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
<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.
Metric status against an objective
<StatCard.Root status="alerting">
<StatCard.Header>
<StatCard.Label>Cancellations</StatCard.Label>
</StatCard.Header>
<StatCard.Value>17</StatCard.Value>
<StatCard.Target>Target: under 10</StatCard.Target>
</StatCard.Root>status sets data-status on the root and tints the frame with the semantic
tokens — alerting destructive, below-objective warning, on-track the
default frame. The tint goes on the frame, not the value, so the number stays
readable and a scan of the grid shows where the fires are.
StatCard.Target is a quiet line under the value for the objective the tint
refers to. The copy is yours — the component only sets the muted small type.
Loading and empty
ChartSkeleton and ChartEmpty (from @hoplallc/chart/react) 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
size="sm" on the root tightens the padding and the gaps; the anatomy is
unchanged.
A number that counts to its new value
Give StatCard.Value a value instead of children and it springs from the
old number to the new one when the data refreshes, with
@hoplallc/motion. format takes an
Intl.NumberFormat or a function. Each frame writes the text directly, so the
card doesn't re-render while it counts. A screen reader hears only the final
value, the server renders it, and reduced motion jumps straight to it.
<StatCard.Value value={projects} format={new Intl.NumberFormat("en-GB")} />A KPI strip
API
StatCard.Root
| Prop | Type | Default | |
|---|---|---|---|
size | "default" | "sm" | — | |
status | "alerting" | "below-objective" | "on-track" | — | Health of the metric against its objective. The tint goes on the frame,
not the value, so the number stays readable and a scan of the grid shows
where the fires are: alerting renders a destructive frame,
below-objective a warning one, on-track keeps the default frame. |
exit | Exit | — | Inside a <Presence>: plays this child's way out before it unmounts. |
render | ComponentRenderFn<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
| Prop | Type | Default | |
|---|---|---|---|
exit | Exit | — | Inside a <Presence>: plays this child's way out before it unmounts. |
Plus every <div> prop. Source: stat-card/components/stat-card-chart.tsx.
StatCard.Delta
| Prop | Type | Default | |
|---|---|---|---|
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. |
exit | Exit | — | Inside a <Presence>: plays this child's way out before it unmounts. |
Plus every <div> prop. Source: stat-card/components/stat-card-delta.tsx.
StatCard.Header
| Prop | Type | Default | |
|---|---|---|---|
exit | Exit | — | Inside a <Presence>: plays this child's way out before it unmounts. |
render | ComponentRenderFn<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
| Prop | Type | Default | |
|---|---|---|---|
exit | Exit | — | Inside a <Presence>: plays this child's way out before it unmounts. |
render | ComponentRenderFn<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.Target
| Prop | Type | Default | |
|---|---|---|---|
exit | Exit | — | Inside a <Presence>: plays this child's way out before it unmounts. |
render | ComponentRenderFn<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-target.tsx.
StatCard.Value
| Prop | Type | Default | |
|---|---|---|---|
format | StatCardFormat | — | How value reads: an Intl.NumberFormat or a function. Defaults to the target's own decimals. |
value | number | — | A number to show instead of children: it springs from the previous value when it changes. |
exit | Exit | — | Inside a <Presence>: plays this child's way out before it unmounts. |
render | ComponentRenderFn<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.