Stat Card React chart component for Tailwind CSS v4
v1.1.0Updated in 1.2.0
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.
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.
Stat Card is a Pro React chart component in the React chart and stat components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add stat-card, together with Animated Counter, Progress, Skeleton, Tooltip, useLocale and 4 more and the npm packages motion, d3-scale, d3-shape and lucide-react.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add stat-cardStat Card is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import StatCard from @/components/ui/stat-card.
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
labeldefault""ReactNodeWhat 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).
valuedefault128450number | string | nullThe 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.
formatdefault"number""number" | "currency" | "percent" | "compact" | "duration" (StatValueFormat)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").
currencydefault"USD"stringISO 4217 code for format="currency" and deltaFormat="currency", printed with its narrow symbol ("$", "€", "£").
decimalsdefault"auto""auto" | "0" | "1" | "2" | "3" | numberFraction 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.
prefixdefault""ReactNodeText before the number for units format does not know, set smaller and muted ("≈").
suffixdefault""ReactNodeText after the number ("km", "/mo", "pcs").
deltadefault12.4number | nullThe 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).
deltaFormatdefault"percent""percent" | "number" | "currency" | "duration" (StatDeltaFormat)Unit of delta: +12.4%, +8, +$1,250, or seconds as a duration (+2 days, 12 hr).
trenddefault"auto""auto" | "up" | "down" | "flat"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).
deltaLabeldefault""ReactNodeThe comparison after the pill, like "vs. last month". Also read in the spoken sentence.
perioddefault""ReactNodeA quiet caption at the end of the header, like "Sept. 2026".
infodefault""ReactNodeWhat the metric means, on an info button beside the label: a tooltip on hover or focus, and on a tap on touch screens.
goaldefault190000numberAdds 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.
emptyValuedefault"–"ReactNodeShown, muted, when value is null or missing. The playground's readout shows it; clear value in code to see it in the card.
sizedefault"md""sm" | "md" | "lg"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).
variantdefault"surface""surface" | "card" | "outline" | "plain"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.
radiusdefault"md""none" | "sm" | "md" | "lg"Corners: 0, 12px, 16px or 24px.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"Accent of the icon tile, the sparkline and the goal bar. The delta never takes it: it is green or red by meaning.
colordefault""stringAny CSS color; replaces the tone for the icon tile, sparkline and goal bar.
iconTonedefault"""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.
symbolDisplaydefault"inline""inline" | "muted" (StatSymbolDisplay)The currency symbol of format="currency": at the number's size and weight ("$0", "$128,450"), or smaller and muted beside it.
flatDeltadefault"label""label" | "value" | "hidden" (StatFlatDelta)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.
sparklineTypedefault"area""line" | "area" | "bar"The built-in sparkline: a line, a line over a soft gradient, or bars with the last one highlighted.
durationDisplaydefault"short""long" | "short" | "narrow"Unit words for format="duration" and deltaFormat="duration": 2 ore 15 minute, 2 ore 15 min., 2 h 15 m.
invertTrenddefaultfalsebooleanDown is good: costs, churn, response time. A drop turns green and a rise red; the arrow still points the real way.
loadingdefaultfalsebooleanSkeletons 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.
animationdefault"count""count" | "roll" | "none"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.
durationdefault900numberMilliseconds of the count (roll uses 70% of it per digit).
localedefault""stringIntl locale for every number. Empty uses the UIProvider locale, then the page language. The demo falls back to en-US.
iconReactNodeThe header icon, in a tile: gray when neutral, tinted with the tone otherwise.
sparklinenumber[]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).
chartReactNodeReplaces the sparkline with your own chart, in the same spot.
goalLabel(percent: number) => ReactNodeThe goal bar caption; defaults to labels.goal ("68% of goal").
formatValue(value: number) => stringFull control over the number text. Called on every frame of the count, so keep it cheap.
formatDelta(delta: number) => stringThe delta text in place of deltaFormat. It gets |delta|; the sign is added (d => `${d} days`).
actionReactNodeA control at the end of the header (a ⋯ menu button). It stays clickable on pressable cards.
hrefdefault""stringMakes 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.
selecteddefaultfalsebooleanFor 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.
linkPropsOmit<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>) => voidMakes the whole card a button (or runs alongside href).
footerReactNodeA row under a hairline that runs edge to edge ("View report →").
childrenReactNodeExtra content under the numbers, above the chart.
labelsPartial<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).
classNamesPartial<Record<"header" | "icon" | "label" | "info" | "period" | "action" | "value" | "unit" | "delta" | "deltaLabel" | "chart" | "goal" | "footer", string>>Extra classes for the inner parts.
classNamestringExtra classes for the card, merged last. For a row of cards use StatGroup (see Parts).
Parts
StatGroup
componentA row of stat cards (role list): a responsive grid of separate tiles, or one joined panel with hairlines between them.
columns{ base?: 1 | 2; sm?: 2 | 3; lg?: 2 | 3 | 4 }Columns per breakpoint: 2 on phones and 4 from 1024px by default.
variantdefault"separate""separate" | "joined"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.
staggerdefaulttruebooleanTiles count up 60ms apart, capped at 12 tiles.
radiusdefault"md""none" | "sm" | "md" | "lg"The joined panel's corners.
Sparkline
componentThe 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.
data*number[]The values, oldest first.
typedefault"area""line" | "area" | "bar"A line, a line with a soft fill below it, or bars.
widthnumber | stringAny CSS length or px; fills the container by default.
heightnumberpx; 32 / 40 / 64 by size when unset.
sizedefault"md""sm" | "md" | "lg"Picks the default height.
tonedefault"neutral"ToneThe stroke and fill color from the design tokens.
colorstringAny CSS color; replaces the tone.
curvedefault"monotone""monotone" | "linear"Smooth or straight segments.
showLastdefaulttruebooleanA dot on the last point (bars: the last bar in full color).
animateddefaulttruebooleanDraws in once when it scrolls into view; off under reduced motion.
getStatTrend
functiongetStatTrend(delta, trend?) returns the arrow direction: "up", "down", "flat", or null when there is no delta.
getStatIntent
functiongetStatIntent(direction, invert?) returns "positive", "negative" or "neutral", flipped when a drop is good (costs, response time).
formatStatDuration
functionformatStatDuration(seconds, locale, display?) formats a duration the way the card does, in "long", "short" or "narrow" units.
DEFAULT_STAT_CARD_LABELS
constantThe 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.
Dependencies
Files
- components/ui/stat-card.tsx
FAQ
Is the React Stat Card chart component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Stat Card?
Run npx wingo-ui@latest add stat-card. It also adds the Wingo UI items it builds on (Animated Counter, Progress, Skeleton, Tooltip, useLocale and 4 more) and the npm packages motion, d3-scale, d3-shape and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/stat-card.
How does Stat Card behave on phones?
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. 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.
See also
Used in
More React chart and stat components
Articles using this component
- React Funnel Chart: Conversion, Drop-off and Cohorts
Component guides9 min read
- Shadcn KPI Cards: Deltas, Sparklines and Goal Bars
Component guides9 min read
- React Animated Number Counter: Rolling Odometer Digits
Component guides9 min read
- React Sparkline: Table Cells, KPI Tiles, Delta Chips
Component guides9 min read