# Stat Card

> A React stat card for dashboards: a label, a big number that counts up, a delta pill that knows good from bad, a sparkline and a goal bar.

- Type: React chart component (Charts & Stats)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.1.0
- Install: `npx wingo-ui@latest add stat-card`
- Docs: https://wingo-ui.com/components/stat-card

## Interaction

It is read, not operated. The first time a card scrolls into view its number counts up from zero (900ms, easing out), the delta pill fades in and rises 4px just after, and the sparkline draws from left to right; later value changes count from the number on screen, and a change that arrives mid-count retargets without a jump. The server and the first paint already show the final numbers, and screen readers hear one sentence with the final value and the change ("$128,450, up 12.4% vs. last month"), never the numbers in between. The delta is green when the change is good and red when it is bad; with invertTrend a drop in costs or response time is green. The info icon opens its explanation on hover, on keyboard focus, and on a tap or long press on phones (a 44px hit area); Escape closes it. With href or onClick the whole card is one link or button (Tab to it, Enter to open): it dips to 0.98 while pressed, lifts on hover, and shows a chevron (always visible on touch); the info button and the action slot stay separately clickable. In a StatGroup the tiles count up 60ms apart (capped at 12 tiles); on phones the group is two columns and md values shrink to fit 360px. Under reduced motion numbers and charts appear final at once. In the playground press Simulate a new day: a new day joins the sparkline and the value and delta count to their new numbers.

## Installation

```bash
npx wingo-ui@latest add stat-card
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/stat-card` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/stat-card.tsx`

npm dependencies: `npm i motion d3-scale d3-shape lucide-react && npm i -D @types/d3-scale @types/d3-shape`

Wingo UI dependencies (installed automatically):

- [Animated Counter](https://wingo-ui.com/components/animated-counter) ([Markdown](https://wingo-ui.com/components/animated-counter.md))
- [Progress](https://wingo-ui.com/components/progress) ([Markdown](https://wingo-ui.com/components/progress.md))
- [Skeleton](https://wingo-ui.com/components/skeleton) ([Markdown](https://wingo-ui.com/components/skeleton.md))
- [Tooltip](https://wingo-ui.com/components/tooltip) ([Markdown](https://wingo-ui.com/components/tooltip.md))
- [useLocale](https://wingo-ui.com/components/use-locale) ([Markdown](https://wingo-ui.com/components/use-locale.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Number format](https://wingo-ui.com/components/number-format) ([Markdown](https://wingo-ui.com/components/number-format.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import { Clock, FileText, UserPlus, Wallet } from "lucide-react"
import { StatCard, StatGroup } from "@/components/ui/stat-card"

export function DashboardKpis({ daily }: { daily: number[] }) {
  return (
    <StatGroup variant="joined">
      <StatCard
        label="Revenue"
        icon={<Wallet />}
        value={128450}
        format="currency"
        delta={12.4}
        deltaLabel="vs. last month"
        sparkline={daily}
        goal={190000}
        locale="en-US"
       
      />
      <StatCard label="Invoices issued" icon={<FileText />} value={342} delta={8} deltaFormat="number" locale="en-US" />
      <StatCard label="New clients" icon={<UserPlus />} value={27} delta={-3.1} locale="en-US" />
      <StatCard
        label="Average time to payment"
        icon={<Clock />}
        value={18 * 86400}
        format="duration"
        delta={-2.5}
        formatDelta={(d) => `${d.toLocaleString("en-US")} days`}
        invertTrend
        locale="en-US"
       
      />
    </StatGroup>
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label?` | `ReactNode` | `""` | What the number is, in the header; truncates with a title on narrow tiles. When empty the playground shows a sample that follows format (Revenue for currency). |
| `value?` | `number \| string \| null` | `128450` | The number. It counts up and is formatted by format; a string renders as is (no animation); null or no value shows emptyValue. The playground starts at a sample 128450; the component's default is null. |
| `format?` | `"number" \| "currency" \| "percent" \| "compact" \| "duration" (StatValueFormat)` | `"number"` | How the number reads, in the locale: 128,450, $128,450 (see symbolDisplay), 12.4% (value 12.4), 128.5K, or a duration from seconds in its largest two units ("2 hr, 15 min", "18 days"). |
| `currency?` | `string` | `"USD"` | ISO 4217 code for format="currency" and deltaFormat="currency", printed with its narrow symbol ("$", "€", "£"). |
| `decimals?` | `"auto" \| "0" \| "1" \| "2" \| "3" \| number` | `"auto"` | Fraction digits of the value. auto: 0 for numbers from 100 up, the currency's digits below 1.000 (0 above), 1 for percent and compact. Fixed from the final value, so the count never changes width mid-way. |
| `prefix?` | `ReactNode` | `""` | Text before the number for units format does not know, set smaller and muted ("≈"). |
| `suffix?` | `ReactNode` | `""` | Text after the number ("km", "/mo", "pcs"). |
| `delta?` | `number \| null` | `12.4` | The change vs the previous period, in deltaFormat units (12.4 is +12.4% with percent). Its sign sets the arrow; \|delta\| below 0.05 is flat. The playground's sample is 12.4; the component has none (no pill). |
| `deltaFormat?` | `"percent" \| "number" \| "currency" \| "duration" (StatDeltaFormat)` | `"percent"` | Unit of delta: +12.4%, +8, +$1,250, or seconds as a duration (+2 days, 12 hr). |
| `trend?` | `"auto" \| "up" \| "down" \| "flat"` | `"auto"` | The arrow. auto reads the sign of delta; set it to force a direction (a pill with only an arrow when there is no delta). For your own delta badges and tables the helpers the card uses are exported too (see Parts). |
| `deltaLabel?` | `ReactNode` | `""` | The comparison after the pill, like "vs. last month". Also read in the spoken sentence. |
| `period?` | `ReactNode` | `""` | A quiet caption at the end of the header, like "Sept. 2026". |
| `info?` | `ReactNode` | `""` | What the metric means, on an info button beside the label: a tooltip on hover or focus, and on a tap on touch screens. |
| `goal?` | `number` | `190000` | Adds a goal bar (value / goal) with "68% of goal" and the formatted target. Numbers only; 0 or less hides it. The playground's sample is 190000; the component has none. |
| `emptyValue?` | `ReactNode` | `"–"` | Shown, muted, when value is null or missing. The playground's readout shows it; clear value in code to see it in the card. |
| `size?` | `"sm" \| "md" \| "lg"` | `"md"` | sm: compact (20px value, p-3, the sparkline at the end of the value row, 48 x 20); md: 28px value, p-4 / p-5, a 40px chart; lg: the hero (32px, 48px from 768px up, p-5 / p-6, a 64px chart). |
| `variant?` | `"surface" \| "card" \| "outline" \| "plain"` | `"surface"` | surface: a raised white panel (dark: #1A1A1A) with the raised shadow; card: a flat gray tile; outline: a hairline border; plain: no container and no padding, for cards inside your own panel. |
| `radius?` | `"none" \| "sm" \| "md" \| "lg"` | `"md"` | Corners: 0, 12px, 16px or 24px. |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | Accent of the icon tile, the sparkline and the goal bar. The delta never takes it: it is green or red by meaning. |
| `color?` | `string` | `""` | Any CSS color; replaces the tone for the icon tile, sparkline and goal bar. |
| `iconTone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `""` | The icon tile's own tint, independent of tone: a green Pro tile or an orange revenue tile over a neutral sparkline. Unset, the tile follows tone. |
| `symbolDisplay?` | `"inline" \| "muted" (StatSymbolDisplay)` | `"inline"` | The currency symbol of format="currency": at the number's size and weight ("$0", "$128,450"), or smaller and muted beside it. |
| `flatDelta?` | `"label" \| "value" \| "hidden" (StatFlatDelta)` | `"label"` | How a change under 0.05 reads: a neutral "No change" pill (labels.unchanged), the zero value ("0%"), or no pill at all. A null delta never shows a pill. |
| `sparklineType?` | `"line" \| "area" \| "bar"` | `"area"` | The built-in sparkline: a line, a line over a soft gradient, or bars with the last one highlighted. |
| `durationDisplay?` | `"long" \| "short" \| "narrow"` | `"short"` | Unit words for format="duration" and deltaFormat="duration": 2 ore 15 minute, 2 ore 15 min., 2 h 15 m. |
| `invertTrend?` | `boolean` | `false` | Down is good: costs, churn, response time. A drop turns green and a rise red; the arrow still points the real way. |
| `loading?` | `boolean` | `false` | Skeletons with the real sizes for the value, delta, chart and goal; the label stays. aria-busy is set. When it turns false the number counts up. |
| `animation?` | `"count" \| "roll" \| "none"` | `"count"` | count: tweens from 0 the first time the card is seen, then from the previous value; roll: the AnimatedCounter odometer for live numbers (numbers and currency; changes roll, the first paint is final); none: static. |
| `duration?` | `number` | `900` | Milliseconds of the count (roll uses 70% of it per digit). |
| `locale?` | `string` | `""` | Intl locale for every number. Empty uses the UIProvider locale, then the page language. The demo falls back to en-US. |
| `icon?` | `ReactNode` |  | The header icon, in a tile: gray when neutral, tinted with the tone otherwise. |
| `sparkline?` | `number[]` |  | Values for the built-in sparkline, oldest first. One point draws a dot; equal values draw a flat line. The chart alone is exported as Sparkline (see Parts). |
| `chart?` | `ReactNode` |  | Replaces the sparkline with your own chart, in the same spot. |
| `goalLabel?` | `(percent: number) => ReactNode` |  | The goal bar caption; defaults to labels.goal ("68% of goal"). |
| `formatValue?` | `(value: number) => string` |  | Full control over the number text. Called on every frame of the count, so keep it cheap. |
| `formatDelta?` | `(delta: number) => string` |  | The delta text in place of deltaFormat. It gets \|delta\|; the sign is added (d => `${d} days`). |
| `action?` | `ReactNode` |  | A control at the end of the header (a ⋯ menu button). It stays clickable on pressable cards. |
| `href?` | `string` | `""` | Makes the whole card a link: press, hover lift, focus outline and a chevron hint (always shown on touch). Its accessible name is the label plus the spoken value and delta. The info button and the action slot stay clickable on top. The demo's link does not navigate. |
| `selected?` | `boolean` | `false` | For a pressable card that filters or picks something (a status tile over a table): a 2px ring in the tone, aria-pressed on the button (onClick) or aria-current on the link (href). Leave it undefined on plain links. |
| `linkProps?` | `Omit<ComponentProps<"a">, "href" \| "onClick" \| "children" \| "className">` |  | Extra attributes for the link that covers an href card: target, rel (target="_blank" gets noopener), download, data-*. |
| `onClick?` | `(event: MouseEvent<HTMLElement>) => void` |  | Makes the whole card a button (or runs alongside href). |
| `footer?` | `ReactNode` |  | A row under a hairline that runs edge to edge ("View report →"). |
| `children?` | `ReactNode` |  | Extra content under the numbers, above the chart. |
| `labels?` | `Partial<StatCardLabels>` |  | Strings: increase / decrease (text => "Up 12.4%"), unchanged ("No change"), goal (percent => "68% of goal"), info ("About this metric"), loading. The English defaults are exported (see Parts). |
| `classNames?` | `Partial<Record<"header" \| "icon" \| "label" \| "info" \| "period" \| "action" \| "value" \| "unit" \| "delta" \| "deltaLabel" \| "chart" \| "goal" \| "footer", string>>` |  | Extra classes for the inner parts. |
| `className?` | `string` |  | Extra classes for the card, merged last. For a row of cards use StatGroup (see Parts). |

## Parts

### StatGroup (component)

A row of stat cards (role list): a responsive grid of separate tiles, or one joined panel with hairlines between them.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `columns?` | `{ base?: 1 \| 2; sm?: 2 \| 3; lg?: 2 \| 3 \| 4 }` |  | Columns per breakpoint: 2 on phones and 4 from 1024px by default. |
| `variant?` | `"separate" \| "joined"` | `"separate"` | Separate tiles, or one panel with hairlines between tiles. |
| `size?` | `"sm" \| "md" \| "lg"` |  | Passed to every card that does not set its own. |
| `animation?` | `"count" \| "roll" \| "none"` |  | Passed to every card that does not set its own. |
| `stagger?` | `boolean` | `true` | Tiles count up 60ms apart, capped at 12 tiles. |
| `radius?` | `"none" \| "sm" \| "md" \| "lg"` | `"md"` | The joined panel's corners. |

### Sparkline (component)

The card's chart on its own, for table cells and list rows. Strokes stay 1.5px at any size, the area gets a soft gradient and it draws in when it scrolls into view; aria-hidden.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `data` | `number[]` |  | The values, oldest first. |
| `type?` | `"line" \| "area" \| "bar"` | `"area"` | A line, a line with a soft fill below it, or bars. |
| `width?` | `number \| string` |  | Any CSS length or px; fills the container by default. |
| `height?` | `number` |  | px; 32 / 40 / 64 by size when unset. |
| `size?` | `"sm" \| "md" \| "lg"` | `"md"` | Picks the default height. |
| `tone?` | `Tone` | `"neutral"` | The stroke and fill color from the design tokens. |
| `color?` | `string` |  | Any CSS color; replaces the tone. |
| `curve?` | `"monotone" \| "linear"` | `"monotone"` | Smooth or straight segments. |
| `showLast?` | `boolean` | `true` | A dot on the last point (bars: the last bar in full color). |
| `animated?` | `boolean` | `true` | Draws in once when it scrolls into view; off under reduced motion. |

### getStatTrend (function)

getStatTrend(delta, trend?) returns the arrow direction: "up", "down", "flat", or null when there is no delta.

### getStatIntent (function)

getStatIntent(direction, invert?) returns "positive", "negative" or "neutral", flipped when a drop is good (costs, response time).

### formatStatDuration (function)

formatStatDuration(seconds, locale, display?) formats a duration the way the card does, in "long", "short" or "narrow" units.

### DEFAULT_STAT_CARD_LABELS (constant)

The English strings: increase and decrease ("Up 12.4%"), "No change", the goal line, the info button and loading.

## Examples

- **Dashboard row**: A joined StatGroup of four KPIs with a period switch; tiles count up 60ms apart.
- **Sizes**: sm, md and lg: compact tiles, the default, and the hero number.
- **Variants**: surface, card, outline and plain, on the page and on a gray card.
- **Loading**: Skeletons with the real sizes, then the numbers count up.
- **Phone grid**: Compact cards two per row at 390px with bar sparklines.
- **Live numbers**: animation="roll": an odometer for values that keep changing.
- **Pressable cards**: href or onClick makes the whole card a link or button, with a footer.
- **Tones and formats**: Brand, info, success and a custom color; percent, compact and duration values.

## See also

- [Progress](https://wingo-ui.com/components/progress) ([Markdown](https://wingo-ui.com/components/progress.md))
- [Animated Counter](https://wingo-ui.com/components/animated-counter) ([Markdown](https://wingo-ui.com/components/animated-counter.md))
- [Sparkline](https://wingo-ui.com/components/sparkline) ([Markdown](https://wingo-ui.com/components/sparkline.md))
- [Trend badge](https://wingo-ui.com/components/trend-badge) ([Markdown](https://wingo-ui.com/components/trend-badge.md))
- [Card](https://wingo-ui.com/components/card) ([Markdown](https://wingo-ui.com/components/card.md))

---

Canonical: https://wingo-ui.com/components/stat-card
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
