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-kpiRequires the @cosskit namespace in your components.json.
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