Stat card
A number with its label, a change with a trend arrow, and a quiet footnote.
npx shadcn@latest add https://ui.goodtabs.co/r/stat-card.json
With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/stat-card
Label, value and a change. The arrow carries the direction, not a colour.
A row of four
Drops into a responsive grid. Flat trends use a dash.
With icon and footnote
An icon top right and one quiet line for the period or source.
Usage
import { StatCard } from "@/components/ui/stat-card"
<StatCard label="Visitors" value="12,480" delta="+6% vs last month" trend="up" />Props
| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | What the number measures. | |
| value | ReactNode | The number, already formatted. | |
| delta | string | Change text shown next to the trend icon. | |
| trend | "up" | "down" | "flat" | "flat" | Direction of the change, drawn as an icon. |
| footnote | string | One quiet line at the bottom. | |
| icon | ReactNode | An icon or small element shown top right. |
Source
stat-card/stat-card.tsx
import * as React from "react";
import { ArrowDownRightIcon, ArrowUpRightIcon, MinusIcon } from "lucide-react";
import { Card, CardContent } from "@/components/ui/card";
import { cn } from "@/lib/utils";
type Trend = "up" | "down" | "flat";
type StatCardProps = Omit<React.ComponentProps<typeof Card>, "children"> & {
/** What the number measures, for example "Monthly visitors". */
label: string;
/** The number, already formatted. */
value: React.ReactNode;
/** Change text next to the trend icon, for example "+12% vs last month". */
delta?: string;
/** Direction of the change. Drawn as an icon, never as a colour. */
trend?: Trend;
/** One quiet line at the bottom, for example the period or the source. */
footnote?: string;
/** An icon or small element shown top right. */
icon?: React.ReactNode;
};
const TREND_ICON = { up: ArrowUpRightIcon, down: ArrowDownRightIcon, flat: MinusIcon } as const;
const TREND_TEXT = { up: "Up", down: "Down", flat: "No change" } as const;
/**
* A number with its label, an optional change and a footnote.
* Monochrome by design: the arrow says the direction, the text says how much.
*/
export function StatCard({ label, value, delta, trend = "flat", footnote, icon, className, ...props }: StatCardProps) {
const TrendIcon = TREND_ICON[trend];
return (
<Card size="sm" className={cn("gap-2", className)} {...props}>
<CardContent className="flex flex-col gap-2">
<div className="flex items-start justify-between gap-3">
<p className="m-0 text-sm text-muted-foreground">{label}</p>
{icon && (
<span
aria-hidden="true"
className="inline-grid size-7 shrink-0 place-items-center rounded-sm bg-muted text-foreground [&_svg]:size-4"
>
{icon}
</span>
)}
</div>
<p className="m-0 text-2xl font-semibold tracking-tight tabular-nums">{value}</p>
{delta && (
<p className="m-0 inline-flex items-center gap-1 text-sm text-muted-foreground">
<TrendIcon className="size-3.5" aria-hidden="true" />
<span className="sr-only">{TREND_TEXT[trend]}: </span>
{delta}
</p>
)}
{footnote && <p className="m-0 text-xs text-muted-foreground">{footnote}</p>}
</CardContent>
</Card>
);
}
For AI agents
Hand your agent this page, the markdown at /components/stat-card/prompt.md, or the Copy AI prompt button above.
When to use
- Dashboards and overview pages that lead with a handful of numbers
- A stats strip under a hero or at the top of a report
- Any place a number needs a label and a sense of direction
When not to use
- Series over time: use a chart, not a row of cards
- More than six numbers side by side, where a table scans faster
Agent notes
- Format the value before passing it; the card does not know about locales or units
- Trend is monochrome on purpose: no green or red, the arrow and the delta text carry the meaning
- It extends shadcn's Card, so className and other div props pass through
- Put several in a CSS grid; the card has no fixed width