UI.MD

Metric Card

A dashboard card for one number: value, goal, delta badge, and its recent trend.

Installation

$
npx shadcn@latest add https://ui.mattdowney.com/r/metric-card.json

Usage

import { MetricCard, Sparkline } from "@/components/ui/metric-card"

Examples

Default

Title, value, goal, distance left, and the trend under it.

Subscribers
183
Goal: 26279 to go

A row of them

Each card picks the chart shape its metric wants: area for revenue, bars for a count per period, a line for a rate.

Monthly revenue
$128,400
vs. last month
Subscribers
183
Goal: 26279 to go
Churn rate
2.4%
vs. last month

With a goal bar

Children join the value block on the bind rung (8px), so a Progress bar reads as part of the number. The chart stays a peer below at 16px.

Monthly revenue
$128,400
86% of goal
x

No trend

Leave trend off and the card is just the number. Nothing collapses or shifts.

Open tickets
17
Across 4 projects

Chart types

bar for a count per period, line for a trend, area for volume under a trend. Line and area mark the last point with a dot; bars put the last one at full accent and step the rest back.

bar
line
area

Chart sizes

24, 40, and 64 pixels tall. Width always fills the container, and a bar is always half its column, so the density holds at any width and any number of points.

sm
default
lg

The chart on its own

Outside a card, where a card would be the wrong box: one row, one series, the number on the end.

Organic search12,840
Direct6,190
Social1,908

No data

An empty series still renders at full height with a hairline on the baseline, so the card does not jump when the numbers arrive.

API Reference

PropTypeDefaultDescription
titlestring-The card header label.
valueReactNode-The headline number.
goalReactNode-The line under the value, e.g. "Goal: 262" or "vs. last month".
delta{ value: number; label?: string }-Distance left to the goal, rendered as "79 to go". Flips to "Goal reached!" at zero or below.
unitstring-Appended to goal, e.g. "lb".
badgeReactNode-Sits inline beside the value. Pass an element: badge={<Badge>+12%</Badge>}.
actionReactNode-Header right slot, for a Kebab or an icon Button.
link{ label: string; onClick: () => void }-Header right slot as a text link. Ignored when action is set.
trend{ data: number[]; type?; size?; label? }-Recent history, drawn under the value by Sparkline. Omit it for a card with no chart.
childrenReactNode-Extra content inside the value block, e.g. a Progress bar. Binds to the value at 8px.
dataSparklinenumber[]-The series, oldest first. An empty array renders the no-data baseline at full height.
typeSparkline"bar" | "line" | "area""bar"bar measures from zero, because a bar's length is the value. line and area measure the min-to-max band, because a trend is about the shape of the change. A series that crosses zero belongs in line; bars clamp negatives to the baseline.
sizeSparkline"sm" | "default" | "lg""default"Height: 24, 40, or 64 pixels. Width always fills the container.
labelSparklinestring-Accessible name. Generated from the data when omitted, in the form "52 points, 62 to 133, trending up".
classNameSparklinestring-Merged onto the root. Set the text color here to retarget every mark: className="text-destructive-500".