Growth Analytics
Track your key metrics and growth over time
Last 30 days
23.5%
Total Users
12,543
+2,401 this month
18.2%
Monthly Revenue
$48,392
+$7,450 this month
5.3%
Active Sessions
3,847
-215 from last week
12.4%
Growth Rate
32.8%
+3.6% increase
Detailed Timeline
View your metrics breakdown over time
| Period | Total Users | Monthly Revenue | Active Sessions | Growth Rate |
|---|---|---|---|---|
| Week 1 | 8.5K+12% | $35K+15% | 4.2K+8% | 28%+5% |
| Week 2 | 9.2K+8.2% | $38.5K+10% | 4.1K-2.4% | 29.5%+5.4% |
| Week 3 | 10.8K+17.4% | $42.1K+9.4% | 4.1K-1.2% | 31.2%+5.8% |
| Week 4 | 12.5K+16.1% | $48.4K+14.9% | 3.8K-5% | 32.8%+5.1% |
Installation
Install dependencies
npm install framer-motion lucide-reactCopy and paste the following code into your project.
"use client";
import React from "react";
import { motion } from "framer-motion";
import {
TrendingUp,
Users,
DollarSign,
Activity,
ArrowUpRight,
ArrowDownRight,
} from "lucide-react";
interface TimelineDataPoint {
period: string;
value: number;
change?: number;
label?: string;
}
interface StatCard {
id: string;
title: string;
value: string;
change: number;
changeLabel: string;
icon: "users" | "revenue" | "activity" | "growth";
timeline: TimelineDataPoint[];
}
interface TimelineStatsProps {
title?: string;
description?: string;
stats: StatCard[];
period?: string;
className?: string;
}
export function TimelineStats({
title = "Growth Analytics",
description = "Track your key metrics over time",
stats,
period = "Last 30 days",
className = "",
}: TimelineStatsProps) {
const getIcon = (iconType: StatCard["icon"]) => {
switch (iconType) {
case "users":
return Users;
case "revenue":
return DollarSign;
case "activity":
return Activity;
case "growth":
return TrendingUp;
}
};
const getIconColor = (iconType: StatCard["icon"]) => {
switch (iconType) {
case "users":
return "bg-blue-500/10 text-blue-600 dark:text-blue-400";
case "revenue":
return "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400";
case "activity":
return "bg-purple-500/10 text-purple-600 dark:text-purple-400";
case "growth":
return "bg-orange-500/10 text-orange-600 dark:text-orange-400";
}
};
const renderMiniChart = (
timeline: TimelineDataPoint[],
isPositive: boolean,
chartId: string,
) => {
if (timeline.length === 0) return null;
const maxValue = Math.max(...timeline.map((d) => d.value));
const minValue = Math.min(...timeline.map((d) => d.value));
const range = maxValue - minValue || 1;
const points = timeline.map((point, index) => {
const x = (index / (timeline.length - 1)) * 100;
const y = 100 - ((point.value - minValue) / range) * 70 - 15;
return `${x},${y}`;
});
const chartColor = isPositive
? "text-emerald-500 dark:text-emerald-400"
: "text-red-500 dark:text-red-400";
return (
<svg
className="h-16 w-full"
viewBox="0 0 100 100"
preserveAspectRatio="none"
>
<defs>
<linearGradient
id={`gradient-${chartId}`}
x1="0%"
y1="0%"
x2="0%"
y2="100%"
>
<stop offset="0%" stopColor="currentColor" stopOpacity="0.3" />
<stop offset="100%" stopColor="currentColor" stopOpacity="0.05" />
</linearGradient>
</defs>
<motion.path
initial={{ pathLength: 0, opacity: 0 }}
animate={{ pathLength: 1, opacity: 1 }}
transition={{ duration: 1.2, ease: "easeOut" }}
d={`M ${points.join(" L ")} L 100,100 L 0,100 Z`}
fill={`url(#gradient-${chartId})`}
className={chartColor}
/>
<motion.polyline
initial={{ pathLength: 0, opacity: 0 }}
animate={{ pathLength: 1, opacity: 1 }}
transition={{ duration: 1.2, ease: "easeOut", delay: 0.1 }}
points={points.join(" ")}
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
className={chartColor}
/>
{points.map((point, index) => {
const [x, y] = point.split(",");
return (
<motion.circle
key={index}
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{
duration: 0.3,
delay: 1.2 + index * 0.1,
ease: "easeOut",
}}
cx={x}
cy={y}
r="2.5"
fill="currentColor"
className={chartColor}
/>
);
})}
</svg>
);
};
return (
<section className={`w-full py-24 ${className}`}>
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, ease: "easeOut" }}
className="mb-12 text-center"
>
<h2 className="mb-4 text-4xl font-bold tracking-tight text-zinc-900 dark:text-white sm:text-5xl">
{title}
</h2>
<p className="mx-auto max-w-2xl text-lg text-zinc-600 dark:text-zinc-400">
{description}
</p>
<div className="mt-4 inline-flex items-center gap-2 rounded-full bg-zinc-100 px-4 py-2 text-sm font-medium text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300">
<Activity className="h-4 w-4" />
{period}
</div>
</motion.div>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
{stats.map((stat, index) => {
const Icon = getIcon(stat.icon);
const isPositive = stat.change >= 0;
const ChangeIcon = isPositive ? ArrowUpRight : ArrowDownRight;
return (
<motion.div
key={stat.id}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{
duration: 0.5,
delay: index * 0.1,
ease: "easeOut",
}}
className="group relative overflow-hidden rounded-2xl border border-zinc-200 bg-white p-6 transition-all hover:shadow-lg dark:border-zinc-800 dark:bg-zinc-900"
>
{/* Icon */}
<div className="mb-4 flex items-center justify-between">
<div
className={`flex h-12 w-12 items-center justify-center rounded-xl ${getIconColor(stat.icon)}`}
>
<Icon className="h-6 w-6" />
</div>
<div
className={`flex items-center gap-1 rounded-full px-2.5 py-1 text-xs font-semibold ${
isPositive
? "bg-emerald-50 text-emerald-700 dark:bg-emerald-950/30 dark:text-emerald-400"
: "bg-red-50 text-red-700 dark:bg-red-950/30 dark:text-red-400"
}`}
>
<ChangeIcon className="h-3 w-3" />
{Math.abs(stat.change)}%
</div>
</div>
<div className="mb-4">
<p className="mb-1 text-sm font-medium text-zinc-600 dark:text-zinc-400">
{stat.title}
</p>
<p className="text-3xl font-bold text-zinc-900 dark:text-white">
{stat.value}
</p>
<p className="mt-1 text-xs text-zinc-500 dark:text-zinc-500">
{stat.changeLabel}
</p>
</div>
<div className="relative h-16 overflow-hidden rounded-lg bg-zinc-50 dark:bg-zinc-800/50">
{renderMiniChart(stat.timeline, isPositive, stat.id)}
</div>
<div className="absolute inset-0 -z-10 bg-gradient-to-br from-blue-500/5 to-purple-500/5 opacity-0 transition-opacity group-hover:opacity-100" />
</motion.div>
);
})}
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, ease: "easeOut" }}
className="mt-12 overflow-hidden rounded-2xl border border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900"
>
<div className="border-b border-zinc-200 p-6 dark:border-zinc-800">
<h3 className="text-xl font-bold text-zinc-900 dark:text-white">
Detailed Timeline
</h3>
<p className="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
View your metrics breakdown over time
</p>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="border-b border-zinc-200 bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-800/50">
<tr>
<th className="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-zinc-600 dark:text-zinc-400">
Period
</th>
{stats.map((stat) => (
<th
key={stat.id}
className="px-6 py-3 text-right text-xs font-semibold uppercase tracking-wider text-zinc-600 dark:text-zinc-400"
>
{stat.title}
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-zinc-200 dark:divide-zinc-800">
{stats[0]?.timeline.map((_, periodIndex) => (
<motion.tr
key={periodIndex}
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{
duration: 0.3,
delay: periodIndex * 0.05,
ease: "easeOut",
}}
className="transition-colors hover:bg-zinc-50 dark:hover:bg-zinc-800/50"
>
<td className="whitespace-nowrap px-6 py-4 text-sm font-medium text-zinc-900 dark:text-white">
{stats[0].timeline[periodIndex].period}
</td>
{stats.map((stat) => {
const dataPoint = stat.timeline[periodIndex];
const change = dataPoint.change;
return (
<td
key={stat.id}
className="whitespace-nowrap px-6 py-4 text-right text-sm"
>
<div className="flex items-center justify-end gap-2">
<span className="font-semibold text-zinc-900 dark:text-white">
{dataPoint.label ||
dataPoint.value.toLocaleString()}
</span>
{change !== undefined && (
<span
className={`text-xs ${
change >= 0
? "text-emerald-600 dark:text-emerald-400"
: "text-red-600 dark:text-red-400"
}`}
>
{change >= 0 ? "+" : ""}
{change}%
</span>
)}
</div>
</td>
);
})}
</motion.tr>
))}
</tbody>
</table>
</div>
</motion.div>
</div>
</section>
);
}Features
- ✅ Multiple stat cards - Display 4+ key metrics at once
- ✅ Mini charts - Visual sparklines for each metric
- ✅ Trend indicators - Show percentage changes with arrows
- ✅ Detailed timeline - Comprehensive data table breakdown
- ✅ Color-coded icons - Different colors for each metric type
- ✅ Responsive design - Works on all screen sizes
- ✅ Dark mode ready - Beautiful in both themes
- ✅ Smooth animations - Framer Motion powered
- ✅ TypeScript support - Full type safety
- ✅ Customizable - Easy to adapt to your needs
Usage
Basic Usage
import TimelineStats from "@/components/ui/timeline-stats";
const stats = [
{
id: "users",
title: "Total Users",
value: "12,543",
change: 23.5,
changeLabel: "+2,401 this month",
icon: "users",
timeline: [
{period: "Week 1", value: 8500, change: 12, label: "8.5K"},
{period: "Week 2", value: 9200, change: 8.2, label: "9.2K"},
{period: "Week 3", value: 10800, change: 17.4, label: "10.8K"},
{period: "Week 4", value: 12543, change: 16.1, label: "12.5K"},
],
},
];
export default function Dashboard() {
return <TimelineStats stats={stats} period="Last 30 days" />;
}With Multiple Metrics
const stats = [
{
id: "users",
title: "Total Users",
value: "12,543",
change: 23.5,
changeLabel: "+2,401 this month",
icon: "users",
timeline: [
/* ... */
],
},
{
id: "revenue",
title: "Monthly Revenue",
value: "$48,392",
change: 18.2,
changeLabel: "+$7,450 this month",
icon: "revenue",
timeline: [
/* ... */
],
},
{
id: "activity",
title: "Active Sessions",
value: "3,847",
change: -5.3,
changeLabel: "-215 from last week",
icon: "activity",
timeline: [
/* ... */
],
},
];
<TimelineStats
title="Growth Analytics"
description="Track your key metrics over time"
stats={stats}
/>;Props
TimelineStatsProps
| Prop | Type | Default | Description |
|---|---|---|---|
stats | StatCard[] | Required | Array of stat cards |
title | string | "Growth Analytics" | Section title |
description | string | "Track your key..." | Section description |
period | string | "Last 30 days" | Time period label |
className | string | "" | Additional CSS classes |
StatCard
| Prop | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique identifier |
title | string | Yes | Stat title |
value | string | Yes | Current value (formatted) |
change | number | Yes | Percentage change |
changeLabel | string | Yes | Change description |
icon | "users" | "revenue" | "activity" | "growth" | Yes | Icon type |
timeline | TimelineDataPoint[] | Yes | Timeline data points |
TimelineDataPoint
| Prop | Type | Required | Description |
|---|---|---|---|
period | string | Yes | Period label (e.g. "Week 1") |
value | number | Yes | Numeric value |
change | number | No | Percentage change |
label | string | No | Formatted display label |
TypeScript Interface
interface TimelineDataPoint {
period: string;
value: number;
change?: number;
label?: string;
}
interface StatCard {
id: string;
title: string;
value: string;
change: number;
changeLabel: string;
icon: "users" | "revenue" | "activity" | "growth";
timeline: TimelineDataPoint[];
}
interface TimelineStatsProps {
title?: string;
description?: string;
stats: StatCard[];
period?: string;
className?: string;
}Use Cases
Perfect for:
- SaaS dashboards - Show key business metrics
- Analytics pages - Display growth trends
- Admin panels - Monitor system performance
- Business intelligence - Track KPIs over time
- Investor reports - Visualize company growth
- Product analytics - Monitor user engagement
- Marketing dashboards - Track campaign performance
- Financial reporting - Display revenue trends
Customization
Add Custom Icons
const getIcon = (iconType: StatCard["icon"]) => {
switch (iconType) {
case "users":
return Users;
case "revenue":
return DollarSign;
case "activity":
return Activity;
case "growth":
return TrendingUp;
case "custom": // Add your own
return YourCustomIcon;
}
};Change Color Scheme
const getIconColor = (iconType: StatCard["icon"]) => {
switch (iconType) {
case "users":
return "bg-blue-500/10 text-blue-600"; // Your brand color
case "revenue":
return "bg-emerald-500/10 text-emerald-600";
// ... more colors
}
};Custom Period Selector
const [period, setPeriod] = useState("30d");
<div className="mb-8 flex justify-center gap-2">
<button
onClick={() => setPeriod("7d")}
className={`rounded-lg px-4 py-2 ${period === "7d" ? "bg-blue-600 text-white" : "bg-zinc-100"}`}
>
7 Days
</button>
<button
onClick={() => setPeriod("30d")}
className={`rounded-lg px-4 py-2 ${period === "30d" ? "bg-blue-600 text-white" : "bg-zinc-100"}`}
>
30 Days
</button>
<button
onClick={() => setPeriod("90d")}
className={`rounded-lg px-4 py-2 ${period === "90d" ? "bg-blue-600 text-white" : "bg-zinc-100"}`}
>
90 Days
</button>
</div>
<TimelineStats stats={stats} period={`Last ${period}`} />Real-Time Data Updates
"use client";
import {useEffect, useState} from "react";
export default function LiveDashboard() {
const [stats, setStats] = useState(initialStats);
useEffect(() => {
// Fetch data every 30 seconds
const interval = setInterval(async () => {
const response = await fetch("/api/analytics");
const data = await response.json();
setStats(data.stats);
}, 30000);
return () => clearInterval(interval);
}, []);
return <TimelineStats stats={stats} />;
}Export Data
const exportToCSV = () => {
const headers = ["Period", ...stats.map((s) => s.title)];
const rows = stats[0].timeline.map((_, i) => [
stats[0].timeline[i].period,
...stats.map((s) => s.timeline[i].value),
]);
const csv = [headers, ...rows].map((row) => row.join(",")).join("\n");
const blob = new Blob([csv], {type: "text/csv"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "analytics.csv";
a.click();
};
<button onClick={exportToCSV}>Export CSV</button>;Common Patterns
Monthly Dashboard
const monthlyStats = [
{
id: "mrr",
title: "Monthly Recurring Revenue",
value: "$125,430",
change: 15.8,
changeLabel: "+$17,120 this month",
icon: "revenue",
timeline: [
{period: "Jan", value: 95000, change: 12, label: "$95K"},
{period: "Feb", value: 102000, change: 7.4, label: "$102K"},
{period: "Mar", value: 115000, change: 12.7, label: "$115K"},
{period: "Apr", value: 125430, change: 9.1, label: "$125K"},
],
},
];User Growth Tracking
const userGrowth = [
{
id: "total-users",
title: "Total Users",
value: "45,231",
change: 28.3,
changeLabel: "+9,987 new users",
icon: "users",
timeline: [
/* weekly data */
],
},
{
id: "active-users",
title: "Active Users (DAU)",
value: "12,543",
change: 15.2,
changeLabel: "+1,658 daily active",
icon: "activity",
timeline: [
/* weekly data */
],
},
];Conversion Funnel
const conversionStats = [
{
id: "visitors",
title: "Website Visitors",
value: "125,430",
change: 23.5,
changeLabel: "+23,890 visitors",
icon: "activity",
timeline: [
/* ... */
],
},
{
id: "signups",
title: "Sign Ups",
value: "12,543",
change: 18.2,
changeLabel: "+1,928 signups",
icon: "users",
timeline: [
/* ... */
],
},
{
id: "conversions",
title: "Paid Conversions",
value: "1,254",
change: 32.1,
changeLabel: "+305 conversions",
icon: "revenue",
timeline: [
/* ... */
],
},
{
id: "conversion-rate",
title: "Conversion Rate",
value: "10.0%",
change: 8.7,
changeLabel: "+0.8% improvement",
icon: "growth",
timeline: [
/* ... */
],
},
];Integration
With API
async function fetchAnalytics(period: string) {
const response = await fetch(`/api/analytics?period=${period}`);
const data = await response.json();
return data.metrics.map((metric) => ({
id: metric.id,
title: metric.name,
value: formatValue(metric.current),
change: metric.percentChange,
changeLabel: metric.changeDescription,
icon: metric.iconType,
timeline: metric.history,
}));
}With Database
// Example with Prisma
async function getGrowthStats() {
const users = await prisma.user.groupBy({
by: ["createdAt"],
_count: true,
orderBy: {createdAt: "asc"},
});
return {
id: "users",
title: "Total Users",
value: users.reduce((sum, day) => sum + day._count, 0).toString(),
timeline: users.map((day) => ({
period: formatDate(day.createdAt),
value: day._count,
})),
};
}