Stat Card

Free · MIT

KPI card with seven variants, tones, shadows and sizes: count-up or rolling NumberFlow values, auto-toned delta badges (invertible for no-show rates), line, area or bar sparklines that draw in, icons, footers and a skeleton state.

MarkdownOpen in v0
Open in v0

Installation

$ npx shadcn@latest add https://goodui.dev/r/stat-card.json

The file lands in components/goodui/stat-card.tsx and is yours to edit. With the @goodui registry added, npx shadcn@latest add @goodui/stat-card works too.

Usage

import { CalendarCheck, DollarSign, UserX } from "lucide-react"
import { StatCard } from "@/components/goodui/stat-card"

export function DashboardStats() {
  return (
    <div className="grid gap-4 sm:grid-cols-3">
      <StatCard
        label="Revenue"
        value={48210}
        format={{ style: "currency", currency: "USD", maximumFractionDigits: 0 }}
        delta={12.4}
        deltaLabel="vs last month"
        data={[31, 34, 33, 38, 41, 39, 45, 48]}
        icon={<DollarSign />}
      />
      <StatCard label="Bookings" value={1284} delta={4.1} data={[38, 42, 40, 51, 47, 55, 61]} chart="bars" icon={<CalendarCheck />} tone="info" />
      <StatCard label="No-show rate" value={3.2} suffix="%" delta={-0.8} invertDelta deltaSuffix=" pts" icon={<UserX />} tone="warning" footer="Deposits enabled since Oct 4" />
    </div>
  )
}

When to use

Use it for
  • The row of headline numbers at the top of a dashboard: revenue, bookings, new clients, no-show rate.
  • A metric with its change since last period and a small trend line, without pulling in a chart library.
  • Metrics that load asynchronously: loading shows a matching skeleton.
  • Live numbers that update in place: numberFlow rolls the digits on every change.
Reach for something else
  • A full chart with axes, tooltips and several series: use a chart component.
  • Just an animated number in a sentence or table cell: use number-flow or counter-up.
  • A trend chip on its own: use delta-badge. A tiny trend line on its own: use sparkline-draw.
If you needUse
Default dashboard rowvariant="outline" (default) with data and chart="area"
One hero metric that stands outvariant="gradient" or shadow="glow"
Calm, tinted cards grouped by categoryvariant="soft" with a tone per group
Icon-led cards in a sidebar or narrow columnvariant="split" with icon, size="sm"
Daily counts (bookings per day)chart="bars"
Lower is better (no-shows, churn, wait time)invertDelta
Real-time valuesnumberFlow (digits roll on each change), animation="draw"
Static reports or printanimation="none"

Examples

Loading state

<StatCard label="Revenue" value={data?.revenue ?? 0} loading={isLoading} data={data?.trend} />

Compact numbers with rolling digits

<StatCard label="Page views" value={views} compact numberFlow variant="soft" tone="success" />

Clickable card

<a href="/reports/revenue" className="block rounded-xl outline-none focus-visible:ring-3 focus-visible:ring-ring/50">
  <StatCard label="Revenue" value={48210} prefix="$" interactive />
</a>

API reference

PropTypeDefault
label*
Metric name shown above the value.
React.ReactNode—
value*
A number (animated and formatted) or any preformatted string. Numeric strings like "1,284" count too.
number | string—
prefix
Text before the formatted number, e.g. "$".
string""
suffix
Text after the formatted number, e.g. "%" or " ms".
string""
format
Intl.NumberFormat options, e.g. { style: "currency", currency: "USD" }.
Intl.NumberFormatOptions—
compact
Short notation: 12.4K, 1.2M.
boolean—
locale
Locale for number formatting. Defaults to the GooduiLocaleProvider language, else en-US.
string—
delta
Change in percent, e.g. 12.5 or -3.
number | string—
deltaLabel
Muted text beside the value, e.g. "vs last month".
React.ReactNode—
deltaSuffix
Unit after the delta number; "%" by default.
string"%"
invertDelta
A drop is good news (cancellations, no-shows, wait time).
boolean—
data
Sparkline points: [12, 18, 15] or "12,18,15".
number[] | string—
chart
Sparkline style for data: line, area (default) or bars.
"line" | "area" | "bars""area"
icon
Icon in a tinted tile beside the label, or in the side block with variant="split".
React.ReactNode—
footer
Muted line at the bottom of the card.
React.ReactNode—
variant
Surface: plain, outline (default), soft, gradient, glass, neo, or split (icon block on the start side).
"plain" | "outline" | "soft" | "gradient" | "glass" | "neo" | "split""outline"
tone
Accent for the icon, sparkline, soft and gradient surfaces and glow. Delta colors are separate (good/bad).
"primary" | "neutral" | "success" | "warning" | "danger" | "info""primary"
shadow
Drop shadow; glow uses the tone. Ignored by neo.
"none" | "sm" | "md" | "lg" | "glow""none"
shape
Corner radius: square, rounded or round.
"square" | "rounded" | "round""rounded"
size"sm" | "md" | "lg""md"
animation
count counts the value up on view, draw draws the sparkline in, full does both.
"none" | "count" | "draw" | "full""full"
numberFlow
Roll digits with NumberFlow.
boolean—
loading
Skeleton state while the metric loads.
boolean—
interactive
Hover lift, for cards that link somewhere.
boolean—
labels
Text for built-in controls and screen readers; pass translations here.
Partial<StatCardLabels>—

Also accepts every <div> attribute.

Accessibility

  • The counting number is aria-hidden; screen readers read the final formatted value from a visually hidden copy, once. With numberFlow, NumberFlow handles its own screen-reader text.
  • The delta adds visually hidden "increase", "decrease" or "no change" after the number, so the trend does not rely on the arrow or color. Translate with labels.
  • The sparkline and its end dot are aria-hidden; describe the trend in deltaLabel or footer if it matters.
  • loading sets aria-busy on the card.
  • interactive only adds a hover lift; the card is a <div>, so wrap it in a link or button for keyboard access.

Motion

  • count: when 30% of the card is in view, the value counts up from 0 over 1.2 s with an ease-out curve; later value changes tween from the current number.
  • draw: the line draws in over 1.1 s, the area fades in, then the end dot pops in at 1.1 s; bars grow from the bottom, 40 ms apart. full (default) does count and draw together.
  • numberFlow: digits roll to the new value on every change instead of tweening.
  • interactive: the card lifts 2px in 200 ms on hover (neo cards grow their offset shadow). The loading skeleton shimmers every 1.4 s.
  • Reduced motion: the value shows the final number at once, the chart is drawn, the skeleton is static and the hover lift does not animate.
  • One IntersectionObserver per card, disconnected after the first reveal. The count-up uses requestAnimationFrame for 1.2 s; the sparkline is measured with a ResizeObserver and redrawn only when its size changes.

Notes for AI

Coding agents read these rules through MCP and llms.txt, or paste this page with Copy page.

  • value can be a number or a string. Numeric strings like "1,284" animate; other strings ("2h 15m", "N/A") are shown as they are.
  • Use format (Intl.NumberFormat options) for currency and percent, or prefix and suffix for simple units. Do not pre-format numbers you want animated.
  • delta is a change in percent by default; set deltaSuffix for other units (" pts", ""). Trend color is automatic: up is good unless invertDelta.
  • data needs at least 2 points to draw a sparkline; it accepts [12, 18, 15] or "12,18,15".
  • Default is variant="outline" tone="primary" chart="area" animation="full" locale="en-US"; pass locale for other number formats. labels translates the delta's screen-reader words.

FAQ

How do I show currency?

Pass format={{ style: "currency", currency: "EUR" }} and optionally locale="de-DE". The count-up formats every frame with the same formatter.

Why is my sparkline missing?

It only renders with 2 or more finite points in data. With loading set it shows a skeleton in its place.

Related