---
id: "packages/active-ui/stat-card"
title: "StatCard"
description: "One key figure on a card - a title, an already formatted value, an optional trend, and optionally a link to the details."
url: "/packages/active-ui/stat-card"
locale: "en"
revision: "0068530215f9a383"
---

# StatCard

`StatCard` shows one key figure of a dashboard: the number of participants, the contributions of a
month. The value arrives formatted. A trend beside it shows the change and whether it is good
news; with `href` the whole card links to the details. For label and value pairs that are not key
figures, use a [DescriptionList](/packages/active-ui/description-list).

**Example: Three key figures**

Client: Runs entirely in your browser. Source: `examples/ui/stat-card.tsx`.

```tsx
// apps/docs/examples/ui/stat-card.tsx
import {FileWarningIcon, UsersIcon, WalletIcon} from 'lucide-react';

import {formatNumber} from '@workspace/active-format';
import {StatCard} from '@workspace/active-ui/components/composed/stat-card';

/**
 * Three key figures. The values arrive formatted; the first card has a
 * rising trend, the second a falling one, and the third links to its
 * details. The figures are fictitious.
 */
export default function StatCardExample() {
  return (
    <div className="grid w-full gap-4 sm:grid-cols-3">
      <StatCard
        title="Participants"
        value={formatNumber(12480, 'integer', 'en')}
        icon={<UsersIcon />}
        trend={{value: '+3.1%', timeframe: 'this year', positive: true}}
      />
      <StatCard
        title="Contributions"
        value="€ 4.2 m"
        icon={<WalletIcon />}
        trend={{value: '-0.8%', timeframe: 'this month', positive: false}}
        description="Received in October"
      />
      <StatCard
        title="Returned statements"
        value={formatNumber(17, 'integer', 'en')}
        icon={<FileWarningIcon />}
        href="#returned"
        description="Open the list"
      />
    </div>
  );
}
```

## What the composition adds

- **A formatted value only**: `value` is a string, so a bare number cannot slip through
  unformatted. Format it with [active-format](/packages/active-format), as the example does.
- **The title as a heading**, `<h2>` by default; `headingLevel={3}` when the cards sit in a section
  that already has an `<h2>`.
- **A trend** with an arrow and a tone: up and green when `positive`, down and red when not, and
  the period beside it.
- **A link card** with `href`, through your router's link with `linkComponent`.

## Usage

```tsx
import {StatCard} from '@workspace/active-ui/components/composed/stat-card';

<StatCard
  title="Participants"
  value={formatNumber(count, 'integer', presentation)}
  trend={{value: '+3.1%', timeframe: 'this year', positive: true}}
/>;
```

## What your application provides

- The formatted value and trend, including its sign: the arrow is decoration and is not read out.
- The heading level that fits the page, and `linkComponent` for client-side navigation.

## Accessibility

- With `href` the card is one link, named by its whole text.
- The icon is decorative and hidden from assistive technology.

## API

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `title` | `string` | yes | The name of the figure, as a heading (see `headingLevel`). Translated. |
| `value` | `string` | yes | The **already formatted** value of the tile.  Deliberately a `string` and not `string \| number`. A bare number is rendered literally by React: the dashboard would show `12480` where the grid next to it reads `12.480`.  The fix is **not** in this component but in the type. A KPI cell does not know what its number means (a count, an already formatted amount, a percentage, an identifier), and a component that guesses the unit will one day guess it wrong. The call site does know, and supplies the text: `formatNumber(count, 'integer', presentation)` for a count, `formatCurrency(...)` for an amount. As a `string`, a bare number is a compiler error, exactly where the semantics are known. |
| `className` | `string` | no | Classes for the card, or for the link around it with `href`. |
| `description` | `string` | no | A short line below the value. |
| `headingLevel` | `StatCardHeadingLevel` | no | The heading level of {@link StatCardProps.title}. Default: `2`.  **Why the default is `2`.** The title is a section heading and not a loose label: the tile is a bounded block with its own subject, and with `href` it is also a destination of its own. Below the `<h1>` of `PageHeader`, `2` is therefore the only level that does not skip.  **When to pass `3`.** Only when the KPI row sits inside a section that is itself already named by an `h2`.  Deliberately no `1` and no `4`-`6`, and deliberately no automatic derivation from a context: a component that guesses its heading level will guess it wrong at the first rearrangement of the page, and the caller is the only one who knows what sits above it. |
| `href` | `string` | no | Makes the whole card a link to this address. |
| `icon` | `ReactNode` | no | A decorative icon beside the title, such as a Lucide icon. |
| `linkComponent` | `LinkComponent` | no | The host's link for {@link StatCardProps.href}, for example `next/link` so navigation stays client-side. Without it the tile is a plain `<a href>` and the browser navigates. Only meaningful with `href`. |
| `trend` | `{ value: string; timeframe: string; positive: boolean; }` | no | A change beside the value: the formatted change (`+4.2%`), the period it covers (`this year`), and whether it is good news, which sets the arrow and the tone. Put the sign in `value`: the arrow is not read out. |

**StatCardHeadingLevel**

```ts
type StatCardHeadingLevel = 2 | 3;
```

The heading level of the title.

Two values and no more, because only two occur: a KPI row sits either
directly below the page heading (`h2`) or inside a section that already
carries an `h2` itself (`h3`).
