Cosskit

KPI Dashboard

Dashboard overview with a KPI stat row, a primary content panel, and a recent activity feed.

pnpm dlx shadcn@latest add @cosskit/dashboard-kpi

Requires the @cosskit namespace in your components.json.

Open full page

Code

import {
  ArrowDownIcon,
  ArrowUpIcon,
  BarChart3Icon,
  CircleUserRoundIcon,
  CreditCardIcon,
  FileTextIcon,
  UserCheckIcon,
  UserMinusIcon,
} from "lucide-react";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Card, CardHeader, CardTitle, CardDescription, CardPanel } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";

// ---------------------------------------------------------------------------
// KPI data
// ---------------------------------------------------------------------------

type DeltaDir = "up" | "down";

interface KpiCard {
  label: string;
  value: string;
  delta: string;
  dir: DeltaDir;
  /** Is "up" semantically good for this metric? (churn: no) */
  upIsGood: boolean;
}

const kpiCards: KpiCard[] = [
  {
    label: "Monthly Recurring Revenue",
    value: "$48,120",
    delta: "+12.4%",
    dir: "up",
    upIsGood: true,
  },
  {
    label: "Active Users",
    value: "3,842",
    delta: "+5.1%",
    dir: "up",
    upIsGood: true,
  },
  {
    label: "Churn Rate",
    value: "1.8%",
    delta: "−0.4%",
    dir: "down",
    upIsGood: false,
  },
  {
    label: "Active Trials",
    value: "214",
    delta: "+18.2%",
    dir: "up",
    upIsGood: true,
  },
];

// ---------------------------------------------------------------------------
// Activity feed data
// ---------------------------------------------------------------------------

interface ActivityItem {
  initials: string;
  name: string;
  action: string;
  time: string;
}

const activityItems: ActivityItem[] = [
  {
    initials: "SP",
    name: "Sarah Park",
    action: "upgraded to the Pro plan",
    time: "2h ago",
  },
  {
    initials: "JM",
    name: "James Mercer",
    action: "started a free trial",
    time: "3h ago",
  },
  {
    initials: "LC",
    name: "Lena Costa",
    action: "invited 3 team members",
    time: "5h ago",
  },
  {
    initials: "RA",
    name: "Raj Anand",
    action: "submitted a support request",
    time: "7h ago",
  },
  {
    initials: "TW",
    name: "Tanya Wolfe",
    action: "cancelled their subscription",
    time: "9h ago",
  },
];

// Map action text to an icon for visual variety
function activityIcon(action: string) {
  if (action.includes("Pro plan") || action.includes("subscription")) return CreditCardIcon;
  if (action.includes("trial")) return UserCheckIcon;
  if (action.includes("invited")) return CircleUserRoundIcon;
  if (action.includes("support")) return FileTextIcon;
  if (action.includes("cancelled")) return UserMinusIcon;
  return CircleUserRoundIcon;
}

// ---------------------------------------------------------------------------
// Delta badge helper
// ---------------------------------------------------------------------------

function DeltaBadge({ card }: { card: KpiCard }) {
  const isPositive =
    (card.dir === "up" && card.upIsGood) ||
    (card.dir === "down" && !card.upIsGood);

  const DirIcon = card.dir === "up" ? ArrowUpIcon : ArrowDownIcon;

  return (
    <Badge variant={isPositive ? "success" : "warning"} size="sm">
      <DirIcon className="size-3" />
      {card.delta}
    </Badge>
  );
}

// ---------------------------------------------------------------------------
// Page
// ---------------------------------------------------------------------------

export default function DashboardKpi() {
  return (
    <div className="flex flex-col gap-8 p-6 md:p-8 [&_[data-slot=button]]:transition-[box-shadow,scale] [&_[data-slot=button]:not(.w-full)]:motion-safe:hover:scale-[1.05] [&_[data-slot=button].w-full]:motion-safe:hover:scale-[1.03]">
      {/* Page heading */}
      <div className="flex flex-col gap-1">
        <h1 className="text-2xl font-semibold tracking-tight">Overview</h1>
        <p className="text-sm text-muted-foreground">
          Key metrics for the last 30 days
        </p>
      </div>

      {/* KPI row — 1 col on mobile, 2 on sm, 4 on lg */}
      <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
        {kpiCards.map((card) => (
          <Card key={card.label}>
            <CardHeader>
              <CardDescription>{card.label}</CardDescription>
              <div className="ms-auto">
                <DeltaBadge card={card} />
              </div>
            </CardHeader>
            <CardPanel>
              <p className="text-3xl font-bold tracking-tight">{card.value}</p>
            </CardPanel>
          </Card>
        ))}
      </div>

      {/* Main content — chart placeholder + activity feed */}
      <div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
        {/* Chart panel — spans 2 of 3 cols on lg */}
        <Card className="lg:col-span-2">
          <CardHeader>
            <CardTitle>Revenue over time</CardTitle>
            <CardDescription>Monthly recurring revenue, last 12 months</CardDescription>
          </CardHeader>
          <CardPanel>
            <div className="flex min-h-64 items-center justify-center rounded-lg border border-dashed">
              <div className="flex flex-col items-center gap-2 text-center">
                <BarChart3Icon className="size-8 text-muted-foreground/50" />
                <p className="text-sm font-medium text-muted-foreground">
                  Chart goes here
                </p>
                <p className="max-w-xs text-xs text-muted-foreground/70">
                  Drop in your charting library — Recharts, Victory, D3, or
                  anything else. This placeholder preserves the layout.
                </p>
              </div>
            </div>
          </CardPanel>
        </Card>

        {/* Recent activity feed */}
        <Card>
          <CardHeader>
            <CardTitle>Recent activity</CardTitle>
            <CardDescription>Latest workspace events</CardDescription>
          </CardHeader>
          <CardPanel>
            <ul className="flex flex-col gap-1">
              {activityItems.map((item, idx) => {
                const Icon = activityIcon(item.action);
                return (
                  <li key={idx}>
                    <div className="flex items-start gap-3 py-2">
                      <Avatar className="size-8 shrink-0 rounded-full">
                        <AvatarFallback className="text-xs">
                          {item.initials}
                        </AvatarFallback>
                      </Avatar>
                      <div className="flex min-w-0 flex-col gap-0.5">
                        <p className="truncate text-sm font-medium leading-none">
                          {item.name}
                        </p>
                        <p className="flex items-center gap-1 text-xs text-muted-foreground">
                          <Icon className="size-3 shrink-0" />
                          <span className="truncate">{item.action}</span>
                        </p>
                      </div>
                      <span className="ms-auto shrink-0 text-xs text-muted-foreground">
                        {item.time}
                      </span>
                    </div>
                    {idx < activityItems.length - 1 && (
                      <Separator />
                    )}
                  </li>
                );
              })}
            </ul>
          </CardPanel>
        </Card>
      </div>
    </div>
  );
}

Dependencies

  • @coss/ui