Skip to content

Dashboard metric row

Four KPI tiles, two of them with inverted trend semantics.

ApplicationstarterFeatureddashboardkpimetricsoverview

Live preview

full widthLive preview — open it in a new tab for the full-height version.
Open the preview in a new tab

Source

This exact file renders the preview above.

import { CreditCard, Timer, Users, Zap } from 'lucide-react'
import { Metric } from '@/components/ui/metric'

/**
 * Dashboard metric row
 *
 * Four KPI tiles, the standard top row of an application overview. Two of them
 * use `invertTrend`, because latency and error rate rising is a regression —
 * a metric row that colours every increase green is actively misleading.
 */
export default function DashboardMetricRow() {
  return (
    <section className="bg-canvas p-4 sm:p-6">
      <h2 className="sr-only">Key metrics</h2>
      <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
        <Metric
          label="Monthly recurring revenue"
          value="$184,200"
          delta={12.4}
          deltaLabel="vs last month"
          icon={<CreditCard className="size-4" />}
          sparkline={[12, 18, 15, 22, 26, 24, 31, 34]}
        />
        <Metric
          label="Active seats"
          value="1,284"
          delta={4.1}
          deltaLabel="vs last month"
          icon={<Users className="size-4" />}
          sparkline={[40, 42, 41, 45, 47, 49, 52, 54]}
        />
        <Metric
          label="p95 latency"
          value="182 ms"
          delta={-8.3}
          deltaLabel="vs last week"
          invertTrend
          icon={<Timer className="size-4" />}
          sparkline={[30, 28, 31, 26, 24, 22, 20, 18]}
        />
        <Metric
          label="Error rate"
          value="0.42%"
          delta={2.7}
          deltaLabel="vs last week"
          invertTrend
          icon={<Zap className="size-4" />}
          sparkline={[8, 7, 9, 11, 10, 12, 13, 15]}
        />
      </div>
    </section>
  )
}

components/blocks/sections/dashboard/metric-row.tsx

import { CreditCard, Timer, Users, Zap } from 'lucide-react'
import { Metric } from '@/components/ui/metric'

/**
 * Dashboard metric row
 *
 * Four KPI tiles, the standard top row of an application overview. Two of them
 * use `invertTrend`, because latency and error rate rising is a regression —
 * a metric row that colours every increase green is actively misleading.
 */
export default function DashboardMetricRow() {
  return (
    <section className="bg-canvas p-4 sm:p-6">
      <h2 className="sr-only">Key metrics</h2>
      <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
        <Metric
          label="Monthly recurring revenue"
          value="$184,200"
          delta={12.4}
          deltaLabel="vs last month"
          icon={<CreditCard className="size-4" />}
          sparkline={[12, 18, 15, 22, 26, 24, 31, 34]}
        />
        <Metric
          label="Active seats"
          value="1,284"
          delta={4.1}
          deltaLabel="vs last month"
          icon={<Users className="size-4" />}
          sparkline={[40, 42, 41, 45, 47, 49, 52, 54]}
        />
        <Metric
          label="p95 latency"
          value="182 ms"
          delta={-8.3}
          deltaLabel="vs last week"
          invertTrend
          icon={<Timer className="size-4" />}
          sparkline={[30, 28, 31, 26, 24, 22, 20, 18]}
        />
        <Metric
          label="Error rate"
          value="0.42%"
          delta={2.7}
          deltaLabel="vs last week"
          invertTrend
          icon={<Zap className="size-4" />}
          sparkline={[8, 7, 9, 11, 10, 12, 13, 15]}
        />
      </div>
    </section>
  )
}

components/ui/metric.tsx

import type { ReactNode } from 'react'
import { ArrowUpRight, ArrowDownRight, Minus } from 'lucide-react'
import { cn } from '@/lib/cn'

/**
 * Metric
 *
 * A single number with optional trend. The delta carries an arrow *and* a sign
 * *and* a colour, so the direction survives a greyscale print or a colour
 * vision deficiency.
 *
 * `<dl>` semantics pair the label with the value; the sparkline is decorative
 * and hidden from assistive tech, since the number beside it is the content.
 */
export interface MetricProps {
  label: string
  value: string
  /** Percentage change. Positive is not automatically "good" — see `invertTrend`. */
  delta?: number
  deltaLabel?: string
  /** For metrics where down is good, e.g. churn or latency. */
  invertTrend?: boolean
  icon?: ReactNode
  /** 0–1 values rendered as a decorative sparkline. */
  sparkline?: number[]
  appearance?: 'plain' | 'card'
  className?: string
}

function Sparkline({ points }: { points: number[] }) {
  if (points.length < 2) return null
  const max = Math.max(...points)
  const min = Math.min(...points)
  const range = max - min || 1
  const path = points
    .map((point, index) => {
      const x = (index / (points.length - 1)) * 100
      const y = 28 - ((point - min) / range) * 26 - 1
      return `${index === 0 ? 'M' : 'L'}${x.toFixed(2)},${y.toFixed(2)}`
    })
    .join(' ')

  return (
    <svg viewBox="0 0 100 28" preserveAspectRatio="none" className="h-7 w-full" aria-hidden="true">
      <path
        d={path}
        fill="none"
        stroke="var(--color-accent)"
        strokeWidth="1.5"
        vectorEffect="non-scaling-stroke"
      />
    </svg>
  )
}

export function Metric({
  label,
  value,
  delta,
  deltaLabel,
  invertTrend = false,
  icon,
  sparkline,
  appearance = 'card',
  className,
}: MetricProps) {
  const direction = delta === undefined ? 'flat' : delta > 0 ? 'up' : delta < 0 ? 'down' : 'flat'
  const good = direction === 'flat' ? null : invertTrend ? direction === 'down' : direction === 'up'
  const TrendIcon =
    direction === 'up' ? ArrowUpRight : direction === 'down' ? ArrowDownRight : Minus

  return (
    <dl
      className={cn(
        'flex min-w-0 flex-col gap-1',
        appearance === 'card' && 'rounded-lg border border-line bg-surface p-card',
        className,
      )}
    >
      <div className="flex items-center justify-between gap-2">
        <dt className="label-caps truncate text-ink-subtle">{label}</dt>
        {icon ? <span className="shrink-0 text-ink-subtle">{icon}</span> : null}
      </div>
      <dd className="display-type text-2xl leading-none font-semibold text-ink-strong tabular-nums">
        {value}
      </dd>
      {delta !== undefined ? (
        <dd
          className={cn(
            'flex items-center gap-1 text-xs font-medium',
            good === null ? 'text-ink-muted' : good ? 'text-success' : 'text-danger',
          )}
        >
          <TrendIcon className="size-3.5 shrink-0" aria-hidden="true" />
          <span>
            {delta > 0 ? '+' : ''}
            {delta}%
          </span>
          {deltaLabel ? <span className="text-ink-subtle">{deltaLabel}</span> : null}
        </dd>
      ) : null}
      {sparkline ? (
        <dd className="mt-1">
          <Sparkline points={sparkline} />
        </dd>
      ) : null}
    </dl>
  )
}

Demo source — adapt to your project. Foundry is not published as a package.

Usage

A metric row that colours every increase green is actively misleading. Latency and error rate use invertTrend, so a rise is correctly shown as a regression.

  • Four tiles. A fifth pushes the row to two lines at every common width.
  • Always pair the delta with a comparison period.

Variants and states

Every entry below is a genuine difference in behaviour or layout, and every one of them is visible in the preview above.

  • Four tiles
  • Sparklines
  • Inverted trends

Accessibility

Description lists
Each tile pairs label and value programmatically.
Decorative sparklines
Sparkline SVGs are aria-hidden; the number is the content.

Foundry implements published ARIA patterns and is tested against them. No WCAG certification is claimed — see the accessibility documentation for what is and is not covered.