Area Chart
Free, copy-pasteable Tailwind CSS Area Chart component. Accessible, fully responsive, dark-mode ready, and customizable.
Install via CLI
Run this command to automatically add the component and its dependencies to your project.
npx @abhaysinghr516/business-wish add area-chartnpx @abhaysinghr516/business-wish init first to initialize your project.Beautifully minimal Area Chart components built on recharts. Every variant ships with fluid monotone curves, transparent gradient fills, a polished custom tooltip, and seamless light/dark mode support.
Four variants cover all common use-cases: a single-series revenue card, a multi-series stacked breakdown, a 100% proportional (percent) view, and a compact sparkline widget.
Basic Area Chart
A clean single-series chart for tracking a key metric over time — here, monthly revenue. Includes a live trend badge comparing the first and second half of the year.
"use client";
import React from "react";
import {
AreaChart, Area, XAxis, YAxis, CartesianGrid,
Tooltip, ResponsiveContainer,
} from "recharts";
import { Activity, TrendingUp, TrendingDown } from "lucide-react";
const totalRevenueData = [
{ name: "Jan", revenue: 12000 }, { name: "Feb", revenue: 15000 },
{ name: "Mar", revenue: 14000 }, { name: "Apr", revenue: 22000 },
{ name: "May", revenue: 18000 }, { name: "Jun", revenue: 28000 },
{ name: "Jul", revenue: 31000 }, { name: "Aug", revenue: 29000 },
{ name: "Sep", revenue: 35000 }, { name: "Oct", revenue: 42000 },
{ name: "Nov", revenue: 38000 }, { name: "Dec", revenue: 48000 },
];
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload?.length) {
return (
<div className="bg-white dark:bg-neutral-900 border border-neutral-200/80 dark:border-neutral-800 p-3 rounded-2xl shadow-lg min-w-[150px]">
<p className="text-[11px] font-semibold text-neutral-400 mb-2 tracking-widest uppercase">{label}</p>
{payload.map((entry: any, i: number) => (
<div key={i} className="flex items-center gap-2.5">
<div className="w-1.5 h-1.5 rounded-full" style={{ backgroundColor: entry.color }} />
<span className="text-[13px] font-medium text-neutral-500 flex-1 capitalize">{entry.name}</span>
<span className="text-[13px] font-bold text-neutral-900 dark:text-white tabular-nums">
${entry.value.toLocaleString()}
</span>
</div>
))}
</div>
);
}
return null;
};
export const BasicAreaChart = ({ height = 320, color = "#007AFF" }) => {
const total = totalRevenueData.reduce((s, d) => s + d.revenue, 0);
const h1 = totalRevenueData.slice(0, 6).reduce((s, d) => s + d.revenue, 0);
const h2 = totalRevenueData.slice(6).reduce((s, d) => s + d.revenue, 0);
const delta = ((h2 - h1) / h1) * 100;
const isUp = delta >= 0;
return (
<div className="w-full bg-white dark:bg-[#0A0A0A] border border-neutral-200 dark:border-neutral-800/80 rounded-3xl p-6 shadow-sm">
<div className="flex items-start justify-between mb-8">
<div>
<div className="flex items-center gap-2 mb-1">
<Activity className="w-3.5 h-3.5 text-neutral-400" strokeWidth={2.5} />
<p className="text-[12px] font-semibold text-neutral-500 uppercase tracking-widest">Total Revenue</p>
</div>
<h3 className="text-[28px] font-bold text-neutral-900 dark:text-white tracking-tighter leading-none">
${(total / 1000).toFixed(0)}k
</h3>
<p className="text-[13px] text-neutral-400 mt-1">Annual performance</p>
</div>
<div className={`flex items-center gap-1.5 px-3 py-1.5 rounded-full text-[12px] font-semibold ${
isUp ? "bg-green-50 dark:bg-green-500/10 text-green-600 dark:text-green-400"
: "bg-red-50 dark:bg-red-500/10 text-red-600 dark:text-red-400"
}`}>
{isUp ? <TrendingUp className="w-3.5 h-3.5" strokeWidth={2.5} />
: <TrendingDown className="w-3.5 h-3.5" strokeWidth={2.5} />}
<span>{Math.abs(delta).toFixed(1)}% H2 vs H1</span>
</div>
</div>
<div style={{ height, width: "100%" }}>
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={totalRevenueData} margin={{ top: 8, right: 4, left: -24, bottom: 0 }}>
<defs>
<linearGradient id="ac-basic-fill" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={color} stopOpacity={0.18} />
<stop offset="100%" stopColor={color} stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#E5E5EA" strokeOpacity={0.8} />
<XAxis dataKey="name" axisLine={false} tickLine={false} tick={{ fontSize: 11, fill: "#8E8E93" }} dy={10} />
<YAxis axisLine={false} tickLine={false} tick={{ fontSize: 11, fill: "#8E8E93" }} tickFormatter={(v) => `$${v / 1000}k`} />
<Tooltip content={<CustomTooltip />} cursor={{ stroke: color, strokeWidth: 1, strokeDasharray: "4 4", strokeOpacity: 0.4 }} />
<Area
type="monotone" dataKey="revenue" name="Revenue"
stroke={color} strokeWidth={2.5} fillOpacity={1} fill="url(#ac-basic-fill)"
animationDuration={1400} animationEasing="ease-out"
activeDot={{ r: 5, fill: color, stroke: "#fff", strokeWidth: 2.5 }}
/>
</AreaChart>
</ResponsiveContainer>
</div>
</div>
);
};
Stacked Area Chart
A dark-surface multi-series chart that stacks three acquisition channels — Organic, Direct, and Referral — into a single continuous area. Ideal for showing proportional volume over time.
"use client";
import React from "react";
import {
AreaChart, Area, XAxis, YAxis, CartesianGrid,
Tooltip, ResponsiveContainer,
} from "recharts";
import { Layers } from "lucide-react";
const trafficSourceData = [
{ name: "Mon", organic: 4200, direct: 2800, referral: 900 },
{ name: "Tue", organic: 5100, direct: 3100, referral: 1100 },
{ name: "Wed", organic: 3800, direct: 2400, referral: 800 },
{ name: "Thu", organic: 6400, direct: 3900, referral: 1500 },
{ name: "Fri", organic: 7200, direct: 4500, referral: 1800 },
{ name: "Sat", organic: 8500, direct: 4100, referral: 2100 },
{ name: "Sun", organic: 6800, direct: 3500, referral: 2400 },
];
const legend = [
{ key: "organic", label: "Organic", color: "#3B82F6" },
{ key: "direct", label: "Direct", color: "#818CF8" },
{ key: "referral", label: "Referral", color: "#C084FC" },
];
export const StackedAreaChart = ({ height = 320 }) => (
<div className="w-full bg-[#0A0A0F] border border-white/[0.06] rounded-3xl p-6 shadow-2xl overflow-hidden">
<div className="flex flex-col sm:flex-row sm:items-start justify-between mb-8 gap-4">
<div>
<div className="flex items-center gap-2 mb-1">
<Layers className="w-3.5 h-3.5 text-neutral-500" strokeWidth={2.5} />
<p className="text-[12px] font-semibold text-neutral-500 uppercase tracking-widest">Traffic Sources</p>
</div>
<h3 className="text-[22px] font-bold text-white tracking-tight">Acquisition Channels</h3>
<p className="text-[13px] text-neutral-500 mt-1">Weekly distribution</p>
</div>
<div className="flex flex-wrap gap-2">
{legend.map((l) => (
<div key={l.key} className="flex items-center gap-1.5 bg-white/5 border border-white/[0.07] px-2.5 py-1 rounded-full">
<div className="w-1.5 h-1.5 rounded-full" style={{ backgroundColor: l.color }} />
<span className="text-[11px] font-medium text-neutral-400">{l.label}</span>
</div>
))}
</div>
</div>
<div style={{ height, width: "100%" }}>
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={trafficSourceData} margin={{ top: 8, right: 4, left: -24, bottom: 0 }}>
<defs>
<linearGradient id="ac-stk-organic" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#3B82F6" stopOpacity={0.45} />
<stop offset="100%" stopColor="#3B82F6" stopOpacity={0.02} />
</linearGradient>
<linearGradient id="ac-stk-direct" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#818CF8" stopOpacity={0.45} />
<stop offset="100%" stopColor="#818CF8" stopOpacity={0.02} />
</linearGradient>
<linearGradient id="ac-stk-referral" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#C084FC" stopOpacity={0.45} />
<stop offset="100%" stopColor="#C084FC" stopOpacity={0.02} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#ffffff" strokeOpacity={0.04} />
<XAxis dataKey="name" axisLine={false} tickLine={false} tick={{ fontSize: 11, fill: "#525252" }} dy={12} />
<YAxis axisLine={false} tickLine={false} tick={{ fontSize: 11, fill: "#525252" }} tickFormatter={(v) => v >= 1000 ? `${v / 1000}k` : v} />
<Tooltip cursor={{ stroke: "#ffffff", strokeWidth: 1, strokeOpacity: 0.08 }} />
<Area type="monotone" dataKey="organic" stackId="1" stroke="#3B82F6" strokeWidth={2} fill="url(#ac-stk-organic)" activeDot={{ r: 4, strokeWidth: 0 }} animationDuration={1000} />
<Area type="monotone" dataKey="direct" stackId="1" stroke="#818CF8" strokeWidth={2} fill="url(#ac-stk-direct)" activeDot={{ r: 4, strokeWidth: 0 }} animationDuration={1000} animationBegin={100} />
<Area type="monotone" dataKey="referral" stackId="1" stroke="#C084FC" strokeWidth={2} fill="url(#ac-stk-referral)" activeDot={{ r: 4, strokeWidth: 0 }} animationDuration={1000} animationBegin={200} />
</AreaChart>
</ResponsiveContainer>
</div>
</div>
);
Percent Area Chart
A 100% stacked area chart that normalises all series to percentages — perfect for tracking relative market share or category proportion over time. Raw data is pre-processed before rendering.
"use client";
import React from "react";
import {
AreaChart, Area, XAxis, YAxis, CartesianGrid,
Tooltip, ResponsiveContainer,
} from "recharts";
import { PieChart } from "lucide-react";
const marketShareData = [
{ name: "Q1", productA: 4000, productB: 2400, productC: 2400 },
{ name: "Q2", productA: 3000, productB: 1398, productC: 2210 },
{ name: "Q3", productA: 2000, productB: 9800, productC: 2290 },
{ name: "Q4", productA: 2780, productB: 3908, productC: 2000 },
{ name: "Q5", productA: 1890, productB: 4800, productC: 2181 },
{ name: "Q6", productA: 2390, productB: 3800, productC: 2500 },
{ name: "Q7", productA: 3490, productB: 4300, productC: 2100 },
];
// Normalise to percentages
const processedMarketData = marketShareData.map((item) => {
const total = item.productA + item.productB + item.productC;
const pct = (v: number) => Number(((v / total) * 100).toFixed(0));
return {
...item,
ProductAPercent: pct(item.productA),
ProductBPercent: pct(item.productB),
ProductCPercent: pct(item.productC),
};
});
const legend = [
{ key: "ProductAPercent", label: "Product A", color: "#10B981" },
{ key: "ProductBPercent", label: "Product B", color: "#14B8A6" },
{ key: "ProductCPercent", label: "Product C", color: "#06B6D4" },
];
export const PercentAreaChart = ({ height = 320 }) => (
<div className="w-full bg-white dark:bg-[#0A0A0A] border border-neutral-200 dark:border-neutral-800/80 rounded-3xl p-6 shadow-sm">
<div className="flex flex-col sm:flex-row sm:items-start justify-between mb-8 gap-4 flex-wrap">
<div>
<div className="flex items-center gap-2 mb-1">
<PieChart className="w-3.5 h-3.5 text-neutral-400" strokeWidth={2.5} />
<p className="text-[12px] font-semibold text-neutral-500 uppercase tracking-widest">Market Share</p>
</div>
<h3 className="text-[22px] font-bold text-neutral-900 dark:text-white tracking-tight">Product Dominance</h3>
<p className="text-[13px] text-neutral-400 mt-1">Relative share over 7 quarters</p>
</div>
<div className="flex flex-wrap gap-2">
{legend.map((l) => (
<div key={l.key} className="flex items-center gap-1.5 bg-neutral-50 dark:bg-neutral-800/60 border border-neutral-200 dark:border-neutral-700/60 px-2.5 py-1 rounded-full">
<div className="w-1.5 h-1.5 rounded-full" style={{ backgroundColor: l.color }} />
<span className="text-[11px] font-medium text-neutral-500 dark:text-neutral-400">{l.label}</span>
</div>
))}
</div>
</div>
<div style={{ height, width: "100%" }}>
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={processedMarketData} margin={{ top: 8, right: 4, left: -24, bottom: 0 }}>
<defs>
<linearGradient id="ac-pct-a" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#10B981" stopOpacity={0.7} />
<stop offset="100%" stopColor="#10B981" stopOpacity={0.15} />
</linearGradient>
<linearGradient id="ac-pct-b" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#14B8A6" stopOpacity={0.7} />
<stop offset="100%" stopColor="#14B8A6" stopOpacity={0.15} />
</linearGradient>
<linearGradient id="ac-pct-c" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#06B6D4" stopOpacity={0.7} />
<stop offset="100%" stopColor="#06B6D4" stopOpacity={0.15} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#E5E5EA" strokeOpacity={0.8} />
<XAxis dataKey="name" axisLine={false} tickLine={false} tick={{ fontSize: 11, fill: "#8E8E93" }} dy={10} />
<YAxis axisLine={false} tickLine={false} tick={{ fontSize: 11, fill: "#8E8E93" }} tickFormatter={(v) => `${v}%`} domain={[0, 100]} />
<Tooltip cursor={{ stroke: "#10B981", strokeWidth: 1, strokeDasharray: "4 4", strokeOpacity: 0.3 }} />
<Area type="monotone" dataKey="ProductAPercent" stackId="1" name="Product A" stroke="#10B981" strokeWidth={1.5} fill="url(#ac-pct-a)" animationDuration={1000} />
<Area type="monotone" dataKey="ProductBPercent" stackId="1" name="Product B" stroke="#14B8A6" strokeWidth={1.5} fill="url(#ac-pct-b)" animationDuration={1000} animationBegin={100} />
<Area type="monotone" dataKey="ProductCPercent" stackId="1" name="Product C" stroke="#06B6D4" strokeWidth={1.5} fill="url(#ac-pct-c)" animationDuration={1000} animationBegin={200} />
</AreaChart>
</ResponsiveContainer>
</div>
</div>
);
Widget Area Chart
A compact, borderless spark-area card designed for tight dashboard grids. Shows the latest value, a percentage delta badge, and a full-bleed sparkline that runs edge-to-edge.
"use client";
import React from "react";
import { AreaChart, Area, Tooltip, ResponsiveContainer, YAxis } from "recharts";
import { Activity, ArrowUpRight } from "lucide-react";
const widgetMetricData = [
{ name: "00:00", value: 120 }, { name: "04:00", value: 150 },
{ name: "08:00", value: 110 }, { name: "12:00", value: 180 },
{ name: "16:00", value: 210 }, { name: "20:00", value: 250 },
{ name: "24:00", value: 220 },
];
export const WidgetAreaChart = ({ color = "#8B5CF6" }) => {
const latest = widgetMetricData[widgetMetricData.length - 1].value;
const prev = widgetMetricData[widgetMetricData.length - 2].value;
const isUp = latest >= prev;
const delta = (((latest - prev) / prev) * 100).toFixed(1);
return (
<div className="w-full max-w-[340px] bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-3xl pt-5 pb-0 px-5 shadow-sm hover:shadow-md transition-shadow duration-300 overflow-hidden">
<div className="flex items-start justify-between mb-5">
<div>
<div className="flex items-center gap-1.5 mb-2">
<Activity className="w-3.5 h-3.5 text-neutral-400" strokeWidth={2.5} />
<p className="text-[11px] font-semibold text-neutral-500 uppercase tracking-widest">Active Users</p>
</div>
<div className="flex items-baseline gap-1.5">
<span className="text-[32px] font-bold text-neutral-900 dark:text-white leading-none tracking-tighter">{latest}</span>
<span className="text-[13px] font-medium text-neutral-400">/hr</span>
</div>
</div>
<div className={`flex items-center gap-1 text-[11px] font-semibold mt-1 px-2 py-1 rounded-full ${
isUp ? "bg-green-50 dark:bg-green-500/10 text-green-600 dark:text-green-400"
: "bg-red-50 dark:bg-red-500/10 text-red-600 dark:text-red-400"
}`}>
<ArrowUpRight className="w-3 h-3" style={{ transform: isUp ? "none" : "rotate(90deg)" }} strokeWidth={2.5} />
<span>{Math.abs(Number(delta))}%</span>
</div>
</div>
<div className="h-[88px] w-[calc(100%+40px)] -mx-5">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={widgetMetricData} margin={{ top: 4, right: 0, left: 0, bottom: 0 }}>
<defs>
<linearGradient id="ac-widget-fill" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={color} stopOpacity={0.22} />
<stop offset="100%" stopColor={color} stopOpacity={0} />
</linearGradient>
</defs>
<YAxis domain={["dataMin - 20", "dataMax + 20"]} hide />
<Tooltip
cursor={{ stroke: color, strokeWidth: 1, strokeOpacity: 0.3 }}
contentStyle={{ borderRadius: "12px", border: "none", boxShadow: "0 4px 20px rgba(0,0,0,0.1)", fontSize: "12px", fontWeight: 600, padding: "6px 10px" }}
itemStyle={{ color: "#171717" }}
labelStyle={{ display: "none" }}
formatter={(v: any) => [`${v}`, "Users"]}
/>
<Area
type="monotone" dataKey="value"
stroke={color} strokeWidth={2}
fillOpacity={1} fill="url(#ac-widget-fill)"
activeDot={{ r: 4, fill: "#fff", stroke: color, strokeWidth: 2 }}
animationDuration={800} dot={false}
/>
</AreaChart>
</ResponsiveContainer>
</div>
</div>
);
};