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.

PropTypeDefaultDescription
isLoadingbooleanfalseWhen 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).

PropTypeDescription
hrefstringWhen 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>
PropTypeDescription
tooltipReactNodeContent 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.

VariantIconColorUse case
positiveTrendingUptext-emerald-400Metric is up and that is good (task throughput, uptime, coverage)
negativeTrendingDowntext-red-400Metric is down or represents an error count increasing
defaultMinustext-foreground-lighterNo 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 positive variant should reflect semantic goodness, not just numeric direction. A rising error count should use negative even 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.

PropTypeDefaultDescription
dataArray<Record<string, string | number>>The data array passed to the recharts chart
dataKeystringKey of the numeric field to plot
colorstring'var(--brand-default)'Stroke and gradient fill color
heightnumber40Height 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>
ComponentSkeleton size when loading
MetricCardValueh-8 w-24
MetricCardDifferentialh-4 w-12
MetricCardSparklineFull 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.