Skip to content
goodtabs UI
BrowseBrowse components

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

Registry installs

1,284

Up: +18% vs last week

Label, value and a change. The arrow carries the direction, not a colour.

A row of four

Visitors

12,480

Up: +6% vs last month

Signups

318

Up: +24 this week

Bounce rate

41%

Down: -3 points

Open issues

7

No change: Same as yesterday

Drops into a responsive grid. Flat trends use a dash.

With icon and footnote

Monthly revenue

$8,420

Up: +$1,120 vs September

Billed through Polar, net of fees

Pro customers

56

Down: 2 refunds

Lifetime licences, as of 29 Oct

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

PropTypeDefaultDescription
labelstringWhat the number measures.
valueReactNodeThe number, already formatted.
deltastringChange text shown next to the trend icon.
trend"up" | "down" | "flat""flat"Direction of the change, drawn as an icon.
footnotestringOne quiet line at the bottom.
iconReactNodeAn 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