Source: https://monarchui.dev/components/stat-card/

Components · Data display

# StatCard

A KPI tile: label, formatted value, change versus a period, hint, help, sparkline and drill-down.

@rookcom/monarch-react

Component

## KPI row with changes

## Usage examples

### Help, unit, sparkline and drill-down

### Sizes and plain variant

### Loading, empty and error

### 0.3 delta props

## 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 `delta` and `change` (DeltaIndicator) announce the direction in words.
- `change` knows 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

```tsx
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 exposes `aria-expanded`.
- Icons, `visual` and `sparkline` are `aria-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, then `columns`.

## Theme support

- Surface, border and text use semantic tokens; status tones use the `-strong` text tokens, which meet AA in light, dark and high-contrast themes.
- `accent` uses `--mn-color-accent-subtle` and `--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 `format` and the viewer’s `locale`, or a value the product has already formatted.
- Set `goodDirection: "down"` in `change` for costs, latency and churn.
- Always pair a change with its comparison period (`caption`).
- Pass `null` when there is no value yet, and `error` when 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 `visual` or `sparkline`; they are hidden from assistive technology.
- Do not use `tone` as 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](https://monarchui.dev/components/delta-indicator/): The change on its own, in tables and rows.
- [Money](https://monarchui.dev/components/money/): How currency values are formatted.
- [Sparkline](https://monarchui.dev/components/sparkline/): The trend line for the `sparkline` slot (`@rookcom/monarch-react/charts`).
- [DescriptionList](https://monarchui.dev/components/description-list/): Several facts about one record.
- [Card](https://monarchui.dev/components/card/): General content surface.

## Dependencies

| Kind | Depends on |
| --- | --- |
| npm packages | `react` |
| Monarch components | [Button](https://monarchui.dev/components/button/), [Money](https://monarchui.dev/components/money/), [DeltaIndicator](https://monarchui.dev/components/delta-indicator/), [InlineErrorRetry](https://monarchui.dev/components/inline-error-retry/), [Skeleton](https://monarchui.dev/components/skeleton/), [VisuallyHidden](https://monarchui.dev/components/visually-hidden/), [Popover](https://monarchui.dev/components/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 `change` with 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`

## All example code

### KPI row with changes

```tsx
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>
```

### Help, unit, sparkline and drill-down

```tsx
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

```tsx
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

```tsx
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

```tsx
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' }}
/>
```
