Components
Metric Cards
MetricCard is a compound component for displaying a single KPI: a label, a large numeric value, an optional change indicator, and an optional sparkline. It is inspired by the Supabase dashboard metric pattern and lives in @repo/ui/components/metric-card.
import {
MetricCard,
MetricCardHeader,
MetricCardLabel,
MetricCardContent,
MetricCardValue,
MetricCardDifferential,
MetricCardSparkline,
} from '@repo/ui/components/metric-card'Anatomy
Each sub-component corresponds to a visual slot. You can use any combination depending on what the metric needs to communicate.
<MetricCard>
<MetricCardHeader>
<MetricCardLabel>Tasks completed</MetricCardLabel>
</MetricCardHeader>
<MetricCardContent>
<MetricCardValue>142</MetricCardValue>
<MetricCardDifferential variant="positive">+12 this week</MetricCardDifferential>
</MetricCardContent>
<MetricCardSparkline data={sparkData} dataKey="count" />
</MetricCard>Components
MetricCard
Root wrapper rendered as a Card. Sets up context with isLoading so child components can coordinate their skeleton states without additional prop threading.
| Prop | Type | Default | Description |
|---|---|---|---|
isLoading | boolean | false | When true, MetricCardValue, MetricCardDifferential, and MetricCardSparkline each render a Skeleton at the appropriate size |
MetricCardHeader
A flex row above the value content. Accepts an optional href — when provided, the header renders as an anchor tag, making the entire header area a clickable link (useful for metrics that navigate to a detail view).
| Prop | Type | Description |
|---|---|---|
href | string | When set, wraps the header in an <a> tag. Use a Next.js Link component in the header instead for client-side navigation. |
MetricCardLabel
The metric name. Renders in text-foreground-lighter text-xs font-medium. Accepts an optional tooltip prop — when provided the label renders with a dashed underline and a Radix Tooltip on hover.
// Simple label
<MetricCardLabel>Tasks completed</MetricCardLabel>
// Label with tooltip
<MetricCardLabel tooltip="Includes all statuses except backlog">
Active tasks
</MetricCardLabel>| Prop | Type | Description |
|---|---|---|
tooltip | ReactNode | Content shown in a TooltipContent popup on hover. Wrap in TooltipProvider at the page level if multiple metric cards are on the same screen. |
MetricCardContent
A flex row that holds the value and differential side by side, aligned to the baseline. No additional props — pass children directly.
MetricCardValue
The primary number display. Styled with font-mono text-2xl font-bold tabular-nums for clean numeric alignment. When isLoading is true on the parent MetricCard, renders a Skeleton sized to h-8 w-24.
<MetricCardValue>1,284</MetricCardValue>
<MetricCardValue>$4.20</MetricCardValue>
<MetricCardValue>98.4%</MetricCardValue>MetricCardDifferential
A change indicator with an arrow icon and colored text. Three variants reflect direction and sentiment. When isLoading is true, renders a Skeleton.
| Variant | Icon | Color | Use case |
|---|---|---|---|
positive | TrendingUp | text-emerald-400 | Metric is up and that is good (task throughput, uptime, coverage) |
negative | TrendingDown | text-red-400 | Metric is down or represents an error count increasing |
default | Minus | text-foreground-lighter | No change, or change direction is neutral / context-dependent |
<MetricCardDifferential variant="positive">+12 this week</MetricCardDifferential>
<MetricCardDifferential variant="negative">−3 vs last week</MetricCardDifferential>
<MetricCardDifferential variant="default">No change</MetricCardDifferential>Semantic note: The
positivevariant should reflect semantic goodness, not just numeric direction. A rising error count should usenegativeeven though the number went up.
MetricCardSparkline
A miniature recharts AreaChart with a gradient fill and no axes. Intended to show trend shape at a glance. Height defaults to 40px to fit naturally beneath the value row. When isLoading is true, renders a skeleton.
| Prop | Type | Default | Description |
|---|---|---|---|
data | Array<Record<string, string | number>> | — | The data array passed to the recharts chart |
dataKey | string | — | Key of the numeric field to plot |
color | string | 'var(--brand-default)' | Stroke and gradient fill color |
height | number | 40 | Height in px |
const sparkData = [
{ week: 'Feb 3', count: 4 },
{ week: 'Feb 10', count: 7 },
{ week: 'Feb 17', count: 5 },
{ week: 'Feb 24', count: 11 },
]
<MetricCardSparkline data={sparkData} dataKey="count" />
// Custom color
<MetricCardSparkline
data={errorData}
dataKey="errors"
color="var(--destructive-default)"
/>Loading states
Set isLoading on the root MetricCard. All child display components automatically render appropriately sized skeletons — no conditional rendering needed in the consumer.
// Loading state — all inner components show skeletons automatically
<MetricCard isLoading>
<MetricCardHeader>
<MetricCardLabel>Tasks completed</MetricCardLabel>
</MetricCardHeader>
<MetricCardContent>
<MetricCardValue>—</MetricCardValue>
<MetricCardDifferential variant="default">—</MetricCardDifferential>
</MetricCardContent>
<MetricCardSparkline data={[]} dataKey="count" />
</MetricCard>| Component | Skeleton size when loading |
|---|---|
MetricCardValue | h-8 w-24 |
MetricCardDifferential | h-4 w-12 |
MetricCardSparkline | Full width, height prop height |
Full example
import {
MetricCard,
MetricCardHeader,
MetricCardLabel,
MetricCardContent,
MetricCardValue,
MetricCardDifferential,
MetricCardSparkline,
} from '@repo/ui/components/metric-card'
function TaskVelocityCard({
weekCount,
prevWeekCount,
sparkData,
isLoading,
}: {
weekCount: number
prevWeekCount: number
sparkData: { week: string; count: number }[]
isLoading: boolean
}) {
const delta = weekCount - prevWeekCount
const variant = delta > 0 ? 'positive' : delta < 0 ? 'negative' : 'default'
const label = delta === 0 ? 'No change' : `${delta > 0 ? '+' : ''}${delta} vs last week`
return (
<MetricCard isLoading={isLoading}>
<MetricCardHeader>
<MetricCardLabel tooltip="Tasks moved to 'done' this calendar week">
This week
</MetricCardLabel>
</MetricCardHeader>
<MetricCardContent>
<MetricCardValue>{weekCount}</MetricCardValue>
<MetricCardDifferential variant={variant}>{label}</MetricCardDifferential>
</MetricCardContent>
<MetricCardSparkline data={sparkData} dataKey="count" />
</MetricCard>
)
}Grid layout
Metric cards work best in a responsive grid. Use Tailwind's grid utilities:
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<MetricCard>…</MetricCard>
<MetricCard>…</MetricCard>
<MetricCard>…</MetricCard>
<MetricCard>…</MetricCard>
</div>Dependency note
MetricCardSparkline uses recharts internally. The same peer dependency requirement as the chart components applies: add recharts >=2.0.0 to the consuming app. If you use MetricCard without MetricCardSparkline, recharts is not required.