Components · Data display
StatCard
A KPI tile: label, formatted value, change versus a period, hint, help, sparkline and drill-down.
KPI row with changes
Revenue
$1,284,500.00
Cost per lead
$41.20
Reply rate
8.1%
212 of 2,617 contacts
import { StatCard, StatCardGroup } from '@rookcom/monarch-react';
<StatCardGroup label="Last 30 days" columns={3} style={{ width: '100%' }}>
<StatCard
label="Revenue"
value={1_284_500}
format="currency"
currency="USD"
notation="compact"
locale="en-US"
change={{ value: 0.124, locale: 'en-US' }}
caption="vs previous 30 days"
/>
<StatCard
label="Cost per lead"
value={41.2}
format="currency"
currency="USD"
locale="en-US"
change={{ current: 41.2, previous: 50.4, goodDirection: 'down', locale: 'en-US' }}
caption="vs previous 30 days"
/>
<StatCard
label="Reply rate"
value={0.081}
format="percent"
locale="en-US"
change={{ value: 0, locale: 'en-US' }}
caption="vs previous 30 days"
hint="212 of 2,617 contacts"
/>
</StatCardGroup>Usage examples
Help, unit, sparkline and drill-down
import { StatCard, StatCardGroup } from '@rookcom/monarch-react';
import { Sparkline } from '@rookcom/monarch-react/charts';
<StatCardGroup columns={2} style={{ width: '100%' }}>
<StatCard
label="First response"
value={2.4}
unit="h"
format="number"
locale="en-US"
help="Median time from a ticket being opened to the first reply by an agent, in business hours."
change={{ current: 2.4, previous: 3.1, goodDirection: 'down', locale: 'en-US' }}
caption="vs last week"
sparkline={
<Sparkline values={[3.4, 3.1, 3.3, 2.9, 2.8, 2.6, 2.4]} responsive height={28} />
}
/>
<StatCard
label="Overdue invoices"
value={7}
href="#/components/stat-card"
tone="danger"
hint="3 more than last month"
/>
</StatCardGroup>Sizes and plain variant
import { StatCard, StatCardGroup } from '@rookcom/monarch-react';
<div style={{ display: 'grid', gap: '1rem', width: '100%' }}>
<StatCard
size="lg"
label="Uptime, 30 days"
value={0.9996}
format="percent"
formatOptions={{ maximumFractionDigits: 2 }}
locale="en-US"
change={{
value: 0.02,
format: 'number',
unit: 'pt',
formatOptions: { maximumFractionDigits: 2 },
locale: 'en-US',
}}
caption="vs previous 30 days"
/>
<StatCardGroup columns={3}>
<StatCard size="sm" label="Open" value="42" />
<StatCard size="sm" label="Pending" value="9" />
<StatCard size="sm" label="Resolved" value="118" tone="success" />
</StatCardGroup>
<div style={{ display: 'flex', gap: '2rem', flexWrap: 'wrap' }}>
<StatCard
variant="plain"
semantics="description-list"
label="Members"
value="24"
hint="3 invited"
/>
<StatCard variant="plain" semantics="description-list" label="Spaces" value="6" />
</div>
</div>Loading, empty and error
import { StatCard, StatCardGroup } from '@rookcom/monarch-react';
<StatCardGroup columns={3} style={{ width: '100%' }}>
<StatCard label="Signups" value={null} loading />
<StatCard label="NPS" value={null} hint="Needs at least 20 responses" />
<StatCard
label="Churn"
value={null}
error="Couldn't load churn."
onRetry={() => undefined}
/>
</StatCardGroup>0.3 delta props
import { StatCard } from '@rookcom/monarch-react';
<StatCard
label="Open tickets"
value="42"
delta="+5"
deltaDirection="up"
deltaTone="danger"
caption="vs last week"
style={{ width: '16rem' }}
/>
<StatCard
label="Resolved"
value="118"
delta="+9"
deltaDirection="up"
caption="vs last week"
style={{ width: '16rem' }}
/>Purpose
A single key figure for a dashboard or overview, with how it changed, what it is compared with, and where to go for detail. StatCardGroup lays several out as a responsive KPI row.
Use when
- Headline metrics on a home, overview or reports page.
- A metric that links to the filtered list behind it ("7 overdue invoices").
- Metrics whose definition needs explaining ("How first response is calculated").
Avoid when
- A trend over time is the point: use a line or bar chart.
- Grouping several facts about one record: use DescriptionList.
- A share of a whole: use StackedProportionBar or a donut.
Design principles
- The product decides what the number is; StatCard only formats it (
format, Intl.NumberFormat, Money for currency) and never invents a change. - Every state is honest: loading shows a placeholder, a missing value shows a dash read as "No data", a failure shows the error with an optional Try again.
- Direction is spoken, not only drawn: both the legacy
deltaandchange(DeltaIndicator) announce the direction in words. changeknows which direction is better (goodDirection), so falling costs are green.- A linked card has one link, named by its label; the whole card is the click target.
- Additive upgrade: the 0.3 props (
delta,deltaDirection,deltaTone,caption,visual) render exactly as before.
API reference
import { StatCard, StatCardGroup } from '@rookcom/monarch-react';| Export | Kind | Rendering | Root class |
|---|---|---|---|
StatCard | component | server-safe | .mn-stat |
StatCardGroup | component | server-safe | .mn-stat |
All native <div> attributes and the ref go to the root .mn-stat.
StatCardGroup is a grid <div>; with label it is a named role="group".
change takes DeltaIndicator props (from @rookcom/monarch-react/charts) without importing that entry.
Generated from the TypeScript declarations of packages/react/src/components/data-display/stat-card.
Properties
With format="percent" the value is a ratio: 0.12 is 12%. Without format, a number is shown as given (0.3 behaviour).
If both delta and change are set, change wins.
StatCard
A single KPI for dashboards: label, value, change, comparison period and optional hint, help, sparkline and drill-down link. Group several with StatCardGroup.
| Prop | Type | Description |
|---|---|---|
label required | ReactNode | |
value required | ReactNode | The value. Pre-formatted text ("₹12.4L", "143"), or a number formatted by `format`. `null` or `undefined` shows the empty state. |
caption | ReactNode | Period label, e.g. "vs last 30 days". With `change`, it becomes the change's spoken comparison. |
change | Omit<DeltaIndicatorProps, "ref"> | A DeltaIndicator in the footer, e.g. `{ value: 0.12 }` or `{ current, previous, goodDirection: 'down' }`. Replaces `delta`; `caption` is used as its comparison unless it has its own. |
children | ReactNode | |
className | string | |
currency | string | ISO 4217 code for `format="currency"`. |
delta | ReactNode | Change versus the previous period, e.g. "+12%". For a computed, polarity-aware change use `change`. |
deltaDirection | "flat" | "up" | "down" | Direction of the change; `up` is success unless `deltaTone` says otherwise. |
deltaTone | "danger" | "success" | "neutral" | Override the colour of the delta, e.g. `danger` when up is bad (churn). |
emptyLabel | string | Spoken for a missing value, shown as a dash. Default "No data". |
error | ReactNode | Replaces the value with this error message. |
format | "number" | "currency" | "percent" | Formats a numeric `value` with `Intl.NumberFormat`: `number`, `percent` (a ratio: 0.12 is 12%) or `currency` (through Money, needs `currency`). Without it a number is shown as given. |
formatOptions | Intl.NumberFormatOptions | Extra `Intl.NumberFormat` options for `format`, e.g. `{ maximumFractionDigits: 1 }`. |
help | ReactNode | How the metric is defined, shown in a popover from an info button next to the label, e.g. "Median time from ticket creation to first agent reply." |
helpLabel | string | Accessible name of the help button. Default "About {label}" when `label` is text, else "About this metric". |
hint | ReactNode | A sub-line under the value, e.g. "38 of 52 people" or "Unique: 1,204". |
href | string | Drill-down: the label becomes a link and the whole card is its click target. |
icon | ReactNode | Decorative icon before the label. |
id | string | |
linkComponent | ElementType | Component used for `href`, e.g. a router link. Default `a`. |
loading | boolean | Shows a placeholder for the value and sets `aria-busy`. |
locale | string | BCP 47 locale for `format`. Default: the runtime locale. |
notation | "compact" | "standard" | `compact` writes 1.2K or $1.2M and gives the full value to assistive technology. Default `standard`. |
onRetry | (() => unknown) | With `error`, shows Try again (InlineErrorRetry) calling this. |
semantics | "text" | "description-list" | `text` (default) renders paragraphs. `description-list` renders the label as `<dt>` and the value lines as `<dd>` inside a `<dl>`. |
size | "sm" | "md" | "lg" | `sm` is compact (dense rows), `md` (default), `lg` is a hero figure. |
sparkline | ReactNode | A trend line under the value (for example a Sparkline). Decorative: the value and change carry the data. |
tone | "danger" | "success" | "warning" | "neutral" | "accent" | Colour role for thresholds or emphasis. `accent` highlights the card; `success`, `warning` and `danger` colour the value. Pair it with words in `hint`: colour is not announced. |
unit | ReactNode | Unit shown smaller after the value, e.g. `ms` or `GB`. |
variant | "card" | "plain" | `card` (default) has a border and surface; `plain` has neither, for use inside another surface. |
visual | ReactNode | Sparkline or icon slot. |
StatCardGroup
A responsive grid of StatCards (a KPI row).
| Prop | Type | Description |
|---|---|---|
children | ReactNode | |
className | string | |
columns | number | Columns on wide screens (from 48rem). Narrower screens use two columns, then one. Default: as many as fit at `minItemWidth`. |
id | string | |
label | string | Names the group (`role="group"`), e.g. "This month". |
minItemWidth | string | Narrowest a card gets in the automatic layout. Default `12rem`. |
Variants
| Prop | Values | Description |
|---|---|---|
size | 'sm''md''lg' | sm is compact for dense rows, md the default, lg a hero figure (data-size). |
variant | 'card''plain' | plain drops the border, padding and surface for use inside another card (data-variant). |
tone | 'neutral''accent''success''warning''danger' | accent tints the card; status tones colour the value and icon for thresholds (data-tone). |
format | 'number''percent''currency' | Formats a numeric value with locale, notation, formatOptions and currency. |
semantics | 'text''description-list' | description-list renders <dl> with the label as <dt> and value lines as <dd>. |
deltaDirection / deltaTone | 'up''down''flat' | The 0.3 pre-formatted delta, still supported. |
States
| State | Exposed as | Behaviour |
|---|---|---|
| Loading | data-state="loading" | A placeholder replaces the value; the footer, hint and sparkline are hidden; aria-busy="true". |
| Empty | data-state="empty" | value null or undefined: a dash read as emptyLabel ("No data"). |
| Error | data-state="error" | error replaces the value; with onRetry it is an InlineErrorRetry with Try again. |
| Linked | data-linked | href makes the label a link stretched over the card. |
| With change | – | A DeltaIndicator (or the 0.3 delta) and the comparison period in the footer. |
Interaction behaviour
- With
href, clicking anywhere on the card follows the link; hover strengthens the border and underlines the label. - With
help, the info button opens a popover with the definition; it closes on Escape or outside click and returns focus to the button. - Otherwise StatCard is read-only.
Keyboard support
| Keys | Action |
|---|---|
| Tab | Moves to the label link (with href), then the help button (with help). |
| Enter | Follows the link, or opens the help popover. |
| Space | Opens the help popover. |
| Escape | Closes the help popover and returns focus to its button. |
A card without href, help or onRetry has no focusable parts.
Accessibility
- Role
- none by default;
semantics="description-list"gives a<dl>term and descriptions - Accessible name
- The label; a linked card’s link is named by the label alone, not the whole card.
- Change direction is spoken ("up 12% vs last month, better"); arrows are
aria-hidden. - Compact values ("1.3M") are read in full ("1,250,000").
- The help button is named "About {label}" (or
helpLabel) and exposesaria-expanded. - Icons,
visualandsparklinearearia-hidden: never put the only copy of data there. - Tone is colour; say what it means in
hint("Above the 5% target"). - Loading sets
aria-busy; the placeholder is hidden from assistive technology. - The linked card shows a focus ring around the whole card when its link has keyboard focus.
Responsive behaviour
- A card fills its grid cell; long labels wrap and the visual slot does not shrink.
- StatCardGroup fits as many cards as
minItemWidth(12rem) allows, down to one column at 320px. - With
columns, it shows one column below 30rem, at most two up to 48rem, thencolumns.
Theme support
- Surface, border and text use semantic tokens; status tones use the
-strongtext tokens, which meet AA in light, dark and high-contrast themes. accentuses--mn-color-accent-subtleand--mn-color-accent-border.- The sparkline slot is coloured
--mn-color-data-1(series palette), never a status colour.
Do and don't
Do
- Pass raw numbers with
formatand the viewer’slocale, or a value the product has already formatted. - Set
goodDirection: "down"inchangefor costs, latency and churn. - Always pair a change with its comparison period (
caption). - Pass
nullwhen there is no value yet, anderrorwhen it failed to load.
Don't
- Do not show a placeholder number (0, 0 MB) when the data is not available.
- Do not put the only copy of information in
visualorsparkline; they are hidden from assistive technology. - Do not use
toneas the only way to show a threshold was crossed. - Do not nest other links or buttons in a linked card apart from
help.
Related components
- DeltaIndicator: The change on its own, in tables and rows.
- Money: How currency values are formatted.
- Sparkline: The trend line for the
sparklineslot (@rookcom/monarch-react/charts). - DescriptionList: Several facts about one record.
- Card: General content surface.
Dependencies
| Kind | Depends on |
|---|---|
| npm packages | react |
| Monarch components | Button, Money, DeltaIndicator, InlineErrorRetry, Skeleton, VisuallyHidden, Popover |
Testing coverage
- Reads the 0.3 delta direction through visually hidden text and applies
deltaTone; legacy markup unchanged. - Formats number, percent, currency (Money) and compact values with the full value spoken.
- Unit and hint; polarity-aware
changewith the caption as its comparison. - Help popover opens with Enter, closes with Escape and returns focus; custom help label.
- Drill-down link named by the label, reachable with Tab, custom link component.
- Loading (aria-busy), empty ("No data") and error with Try again.
- description-list semantics, tone/size/variant attributes, ref forwarding.
- StatCardGroup group role, column variables and minimum width.
| Check | Covered |
|---|---|
| Axe in jsdom | Yes |
| Server rendering | Yes |
| Browser suites | Visual baselines: /__gallery/stat-card; Cross-browser axe: /components/stat-card |
Tests: packages/react/src/components/data-display/stat-card