Stats & Metrics Row

A flexible metrics dashboard component inspired by Vercel. Shows key numbers with trend indicators, loading skeletons, and responsive column layouts.

Dashboard StatsInspired by Vercel
Live PreviewInteractive
Dashboard Preview

Website Analytics

Total Visits
24,521
+12.5%vs last month
Unique Visitors
18,340
+8.2%vs last month
Bounce Rate
42.3%
-3.1%vs last month
Avg Session
2m 34s
+0.8%vs last month

Revenue Metrics

MRR
$4,290
+22.4%vs last month
ARR
$51,480
+22.4%vs last month
Churn Rate
1.8%
-0.4%vs last month
Active Users
312
+15.7%vs last month
StatsRow.tsx
// Dependencies: react ^18, lucide-react
import React from 'react';
import { TrendingUp, TrendingDown, Minus } from 'lucide-react';
import type { LucideIcon } from 'lucide-react';

type StatItem = {
  id: string;
  label: string;
  value: string | number;
  change?: number;
  changeLabel?: string;
  trend?: 'up' | 'down' | 'neutral';
  prefix?: string;
  suffix?: string;
  icon?: LucideIcon;
};

type Props = {
  stats: StatItem[];
  columns?: 2 | 3 | 4;
  loading?: boolean;
};

const colClass: Record<number, string> = {
  2: 'grid-cols-1 sm:grid-cols-2',
  3: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',
  4: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4',
};

export function StatsRow({ stats, columns = 4, loading = false }: Props) {
  return (
    <div className={`grid gap-3 ${colClass[columns]}`}>
      {loading
        ? Array.from({ length: columns }).map((_, i) => <SkeletonCard key={i} />)
        : stats.map((stat) => <StatCard key={stat.id} stat={stat} />)}
    </div>
  );
}

function StatCard({ stat }: { stat: StatItem }) {
  const Icon = stat.icon;
  const hasTrend = stat.trend !== undefined && stat.change !== undefined;

  return (
    <div className="group flex flex-col gap-3 rounded-xl border border-[#2a2a2a] bg-[#111111] p-4 hover:-translate-y-0.5 hover:border-[#3a3a3a] transition-all duration-200">
      <div className="flex items-center justify-between">
        <span className="text-xs text-zinc-500 font-medium">{stat.label}</span>
        {Icon && (
          <div className="w-7 h-7 rounded-lg bg-zinc-900 border border-[#2a2a2a] flex items-center justify-center">
            <Icon className="w-3.5 h-3.5 text-zinc-500" />
          </div>
        )}
      </div>

      <div className="flex items-baseline gap-1">
        {stat.prefix && <span className="text-sm text-zinc-500">{stat.prefix}</span>}
        <span className="text-3xl font-bold text-white tracking-tight leading-none">
          {stat.value}
        </span>
        {stat.suffix && <span className="text-sm text-zinc-500">{stat.suffix}</span>}
      </div>

      <div className="flex items-center gap-1.5 min-h-[18px]">
        {hasTrend ? (
          <>
            {stat.trend === 'up' && <TrendingUp className="w-3.5 h-3.5 text-emerald-400 shrink-0" />}
            {stat.trend === 'down' && <TrendingDown className="w-3.5 h-3.5 text-red-400 shrink-0" />}
            {stat.trend === 'neutral' && <Minus className="w-3.5 h-3.5 text-zinc-500 shrink-0" />}
            <span
              className={`text-xs font-semibold ${
                stat.trend === 'up' ? 'text-emerald-400'
                  : stat.trend === 'down' ? 'text-red-400'
                  : 'text-zinc-500'
              }`}
            >
              {stat.change! > 0 ? '+' : ''}{stat.change}%
            </span>
            {stat.changeLabel && (
              <span className="text-xs text-zinc-600">{stat.changeLabel}</span>
            )}
          </>
        ) : (
          <span className="text-xs text-zinc-700">—</span>
        )}
      </div>
    </div>
  );
}

function SkeletonCard() {
  return (
    <div className="flex flex-col gap-3 rounded-xl border border-[#2a2a2a] bg-[#111111] p-4">
      <div className="flex items-center justify-between">
        <div className="h-2.5 w-20 rounded bg-zinc-800 animate-pulse" />
        <div className="w-7 h-7 rounded-lg bg-zinc-800 animate-pulse" />
      </div>
      <div className="h-8 w-28 rounded bg-zinc-800 animate-pulse" />
      <div className="h-2.5 w-24 rounded bg-zinc-800 animate-pulse" />
    </div>
  );
}

// Usage example:
// const stats = [
//   { id: 'visits', label: 'Total Visits', value: '24,521', change: 12.5, changeLabel: 'vs last month', trend: 'up' },
//   { id: 'mrr', label: 'MRR', value: '4,290', prefix: '$', change: 22.4, trend: 'up' },
// ];
// <StatsRow stats={stats} columns={4} loading={false} />

Unlock to copy

Free access to all patterns