Area Chart

Component

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-chart
New to the CLI? Run npx @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>
  );
};