Dashboard Revenue
Original · freerevenue overview card with chart
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/dash-revenue.json"use client";
import { useCallback, useId, useMemo, useRef, useState } from "react";
import type {
KeyboardEvent as ReactKeyboardEvent,
PointerEvent as ReactPointerEvent,
} from "react";
import { motion, useReducedMotion } from "motion/react";
type RangeId = "7d" | "30d" | "12m";
type Point = {
/** Short axis label. */
label: string;
/** Full label used in the tooltip and the screen-reader table. */
full: string;
value: number;
prev: number;
};
type RangeDef = {
id: RangeId;
label: string;
/** How the comparison period is described in prose. */
comparison: string;
points: Point[];
};
type Channel = {
name: string;
share: number;
delta: number;
bar: string;
dot: string;
};
const RANGES: RangeDef[] = [
{
id: "7d",
label: "7 days",
comparison: "the 7 days before",
points: [
{ label: "Sat", full: "Saturday 11 July", value: 8420, prev: 7180 },
{ label: "Sun", full: "Sunday 12 July", value: 7160, prev: 6240 },
{ label: "Mon", full: "Monday 13 July", value: 13240, prev: 10980 },
{ label: "Tue", full: "Tuesday 14 July", value: 14880, prev: 11640 },
{ label: "Wed", full: "Wednesday 15 July", value: 12960, prev: 11020 },
{ label: "Thu", full: "Thursday 16 July", value: 15310, prev: 12470 },
{ label: "Fri", full: "Friday 17 July", value: 11840, prev: 9860 },
],
},
{
id: "30d",
label: "30 days",
comparison: "the 30 days before",
points: [
{ label: "18 Jun", full: "Thursday 18 June", value: 12480, prev: 10240 },
{ label: "19 Jun", full: "Friday 19 June", value: 11920, prev: 9880 },
{ label: "20 Jun", full: "Saturday 20 June", value: 8140, prev: 6920 },
{ label: "21 Jun", full: "Sunday 21 June", value: 7480, prev: 6310 },
{ label: "22 Jun", full: "Monday 22 June", value: 13060, prev: 10720 },
{ label: "23 Jun", full: "Tuesday 23 June", value: 14210, prev: 11380 },
{ label: "24 Jun", full: "Wednesday 24 June", value: 13740, prev: 11150 },
{ label: "25 Jun", full: "Thursday 25 June", value: 12890, prev: 10640 },
{ label: "26 Jun", full: "Friday 26 June", value: 11460, prev: 9720 },
{ label: "27 Jun", full: "Saturday 27 June", value: 7920, prev: 6840 },
{ label: "28 Jun", full: "Sunday 28 June", value: 7310, prev: 6180 },
{ label: "29 Jun", full: "Monday 29 June", value: 13580, prev: 11090 },
{ label: "30 Jun", full: "Tuesday 30 June", value: 15640, prev: 12360 },
{ label: "1 Jul", full: "Wednesday 1 July", value: 16820, prev: 13240 },
{ label: "2 Jul", full: "Thursday 2 July", value: 14360, prev: 11720 },
{ label: "3 Jul", full: "Friday 3 July", value: 12740, prev: 10380 },
{ label: "4 Jul", full: "Saturday 4 July", value: 8260, prev: 7040 },
{ label: "5 Jul", full: "Sunday 5 July", value: 7690, prev: 6420 },
{ label: "6 Jul", full: "Monday 6 July", value: 13920, prev: 11240 },
{ label: "7 Jul", full: "Tuesday 7 July", value: 14680, prev: 11860 },
{ label: "8 Jul", full: "Wednesday 8 July", value: 14120, prev: 11430 },
{ label: "9 Jul", full: "Thursday 9 July", value: 13380, prev: 10920 },
{ label: "10 Jul", full: "Friday 10 July", value: 12240, prev: 10060 },
{ label: "11 Jul", full: "Saturday 11 July", value: 8420, prev: 7180 },
{ label: "12 Jul", full: "Sunday 12 July", value: 7160, prev: 6240 },
{ label: "13 Jul", full: "Monday 13 July", value: 13240, prev: 10980 },
{ label: "14 Jul", full: "Tuesday 14 July", value: 14880, prev: 11640 },
{ label: "15 Jul", full: "Wednesday 15 July", value: 12960, prev: 11020 },
{ label: "16 Jul", full: "Thursday 16 July", value: 15310, prev: 12470 },
{ label: "17 Jul", full: "Friday 17 July", value: 11840, prev: 9860 },
],
},
{
id: "12m",
label: "12 months",
comparison: "the same months last year",
points: [
{ label: "Aug", full: "August 2025", value: 186400, prev: 142300 },
{ label: "Sep", full: "September 2025", value: 194200, prev: 151800 },
{ label: "Oct", full: "October 2025", value: 178900, prev: 139400 },
{ label: "Nov", full: "November 2025", value: 212600, prev: 168200 },
{ label: "Dec", full: "December 2025", value: 268300, prev: 205600 },
{ label: "Jan", full: "January 2026", value: 241800, prev: 187300 },
{ label: "Feb", full: "February 2026", value: 229400, prev: 176900 },
{ label: "Mar", full: "March 2026", value: 254700, prev: 194500 },
{ label: "Apr", full: "April 2026", value: 271200, prev: 208700 },
{ label: "May", full: "May 2026", value: 288500, prev: 221400 },
{ label: "Jun", full: "June 2026", value: 305900, prev: 233800 },
{ label: "Jul", full: "July 2026", value: 342600, prev: 249100 },
],
},
];
const CHANNELS: Channel[] = [
{
name: "Subscriptions",
share: 0.58,
delta: 14.2,
bar: "bg-indigo-500",
dot: "bg-indigo-500",
},
{
name: "One-time licences",
share: 0.21,
delta: 6.8,
bar: "bg-violet-500",
dot: "bg-violet-500",
},
{
name: "Professional services",
share: 0.13,
delta: 21.5,
bar: "bg-sky-500",
dot: "bg-sky-500",
},
{
name: "Marketplace apps",
share: 0.08,
delta: -3.4,
bar: "bg-amber-500",
dot: "bg-amber-500",
},
];
const money = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 0,
});
const moneyCompact = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
notation: "compact",
maximumFractionDigits: 1,
});
const VB_W = 760;
const VB_H = 280;
const PAD_TOP = 18;
const PAD_RIGHT = 18;
const PAD_BOTTOM = 34;
const PAD_LEFT = 58;
const PLOT_W = VB_W - PAD_LEFT - PAD_RIGHT;
const PLOT_H = VB_H - PAD_TOP - PAD_BOTTOM;
const BASE_Y = PAD_TOP + PLOT_H;
const NICE_STEPS = [1, 1.2, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10];
function niceCeil(value: number): number {
if (value <= 0) return 1;
const magnitude = Math.pow(10, Math.floor(Math.log10(value)));
const scaled = value / magnitude;
const step = NICE_STEPS.find((candidate) => scaled <= candidate) ?? 10;
return step * magnitude;
}
function buildLine(coords: Array<{ x: number; y: number }>): string {
if (coords.length === 0) return "";
let d = `M ${coords[0].x} ${coords[0].y}`;
for (let i = 1; i < coords.length; i += 1) {
const from = coords[i - 1];
const to = coords[i];
const midX = (from.x + to.x) / 2;
d += ` C ${midX} ${from.y} ${midX} ${to.y} ${to.x} ${to.y}`;
}
return d;
}
function formatDelta(delta: number): string {
return `${delta >= 0 ? "+" : "−"}${Math.abs(delta).toFixed(1)}%`;
}
const KEYFRAMES = `
@keyframes dashrev-ping {
0% { transform: scale(1); opacity: 0.65; }
70% { transform: scale(2.6); opacity: 0; }
100% { transform: scale(2.6); opacity: 0; }
}
.dashrev-ping { animation: dashrev-ping 2.4s cubic-bezier(0, 0, 0.2, 1) infinite; }
@media (prefers-reduced-motion: reduce) {
.dashrev-ping { animation: none !important; }
}
`;
export default function DashRevenue() {
const rawId = useId();
const uid = rawId.replace(/[^a-zA-Z0-9]/g, "");
const reduceMotion = useReducedMotion();
const [rangeId, setRangeId] = useState<RangeId>("30d");
const [compare, setCompare] = useState(true);
const [activeIndex, setActiveIndex] = useState<number | null>(null);
const tabRefs = useRef<Array<HTMLButtonElement | null>>([]);
const range = useMemo(
() => RANGES.find((entry) => entry.id === rangeId) ?? RANGES[1],
[rangeId],
);
const chart = useMemo(() => {
const points = range.points;
const count = points.length;
const peak = Math.max(
...points.map((point) => Math.max(point.value, point.prev)),
);
const max = niceCeil(peak);
const step = count > 1 ? PLOT_W / (count - 1) : 0;
const toX = (index: number) => PAD_LEFT + index * step;
const toY = (value: number) => PAD_TOP + (1 - value / max) * PLOT_H;
const current = points.map((point, index) => ({
x: toX(index),
y: toY(point.value),
}));
const previous = points.map((point, index) => ({
x: toX(index),
y: toY(point.prev),
}));
const line = buildLine(current);
const area =
current.length > 0
? `${line} L ${current[current.length - 1].x} ${BASE_Y} L ${current[0].x} ${BASE_Y} Z`
: "";
const total = points.reduce((sum, point) => sum + point.value, 0);
const prevTotal = points.reduce((sum, point) => sum + point.prev, 0);
const delta = prevTotal === 0 ? 0 : ((total - prevTotal) / prevTotal) * 100;
const best = points.reduce(
(top, point) => (point.value > top.value ? point : top),
points[0],
);
const ticks = Array.from({ length: 5 }, (_, i) => {
const value = (max / 4) * i;
return { value, y: toY(value) };
});
const labelEvery = count > 12 ? 5 : 1;
return {
points,
count,
max,
step,
current,
previous,
line,
area,
prevLine: buildLine(previous),
total,
prevTotal,
delta,
best,
ticks,
labelEvery,
};
}, [range]);
const active = activeIndex === null ? null : chart.points[activeIndex];
const activeCoord = activeIndex === null ? null : chart.current[activeIndex];
const pickNearest = useCallback(
(clientX: number, rect: DOMRect) => {
if (rect.width === 0) return;
const ratio = (clientX - rect.left) / rect.width;
const vx = ratio * VB_W;
const raw = chart.step === 0 ? 0 : (vx - PAD_LEFT) / chart.step;
const index = Math.min(chart.count - 1, Math.max(0, Math.round(raw)));
setActiveIndex(index);
},
[chart.count, chart.step],
);
const handlePointerMove = useCallback(
(event: ReactPointerEvent<SVGSVGElement>) => {
pickNearest(event.clientX, event.currentTarget.getBoundingClientRect());
},
[pickNearest],
);
const handleChartKeyDown = useCallback(
(event: ReactKeyboardEvent<HTMLDivElement>) => {
const { key } = event;
if (
key !== "ArrowLeft" &&
key !== "ArrowRight" &&
key !== "Home" &&
key !== "End" &&
key !== "Escape"
) {
return;
}
event.preventDefault();
if (key === "Escape") {
setActiveIndex(null);
return;
}
if (key === "Home") {
setActiveIndex(0);
return;
}
if (key === "End") {
setActiveIndex(chart.count - 1);
return;
}
const direction = key === "ArrowRight" ? 1 : -1;
setActiveIndex((prev) => {
if (prev === null) return direction === 1 ? 0 : chart.count - 1;
return Math.min(chart.count - 1, Math.max(0, prev + direction));
});
},
[chart.count],
);
const handleTabKeyDown = useCallback(
(event: ReactKeyboardEvent<HTMLButtonElement>, index: number) => {
const { key } = event;
if (
key !== "ArrowLeft" &&
key !== "ArrowRight" &&
key !== "Home" &&
key !== "End"
)
return;
event.preventDefault();
let next = index;
if (key === "ArrowRight") next = (index + 1) % RANGES.length;
if (key === "ArrowLeft")
next = (index - 1 + RANGES.length) % RANGES.length;
if (key === "Home") next = 0;
if (key === "End") next = RANGES.length - 1;
const target = RANGES[next];
setRangeId(target.id);
setActiveIndex(null);
tabRefs.current[next]?.focus();
},
[],
);
const selectRange = useCallback((id: RangeId) => {
setRangeId(id);
setActiveIndex(null);
}, []);
const up = chart.delta >= 0;
const tooltipLeft = activeCoord
? Math.min(93, Math.max(7, (activeCoord.x / VB_W) * 100))
: 0;
return (
<section className="relative w-full bg-slate-50 px-4 py-16 sm:px-6 sm:py-20 dark:bg-slate-950">
<style>{KEYFRAMES}</style>
<div className="mx-auto w-full max-w-5xl">
<div className="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
{/* Header */}
<div className="flex flex-col gap-6 border-b border-slate-200 p-6 sm:p-8 dark:border-slate-800">
<div className="flex flex-col gap-5 sm:flex-row sm:items-start sm:justify-between">
<div>
<div className="flex items-center gap-2">
<span className="relative flex h-2 w-2">
<span className="dashrev-ping absolute inline-flex h-full w-full rounded-full bg-emerald-500" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-emerald-500" />
</span>
<span className="text-xs font-medium tracking-wide text-slate-500 uppercase dark:text-slate-400">
Live · synced 2 minutes ago
</span>
</div>
<h2 className="mt-3 text-lg font-semibold text-slate-900 dark:text-slate-50">
Revenue overview
</h2>
<p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
Net of refunds and processing fees · USD
</p>
<div className="mt-5 flex flex-wrap items-center gap-x-3 gap-y-2">
<span className="text-3xl font-semibold tracking-tight tabular-nums text-slate-900 sm:text-4xl dark:text-slate-50">
{money.format(chart.total)}
</span>
<span
className={[
"inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-semibold tabular-nums",
up
? "bg-emerald-50 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-400"
: "bg-rose-50 text-rose-700 dark:bg-rose-500/10 dark:text-rose-400",
].join(" ")}
>
<svg
viewBox="0 0 16 16"
className={["h-3 w-3", up ? "" : "rotate-180"].join(" ")}
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M8 13V3" />
<path d="M3.5 7.5 8 3l4.5 4.5" />
</svg>
{formatDelta(chart.delta)}
</span>
</div>
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400">
vs{" "}
<span className="font-medium tabular-nums text-slate-700 dark:text-slate-300">
{money.format(chart.prevTotal)}
</span>{" "}
in {range.comparison}
</p>
</div>
{/* Range tabs */}
<div
role="tablist"
aria-label="Time range"
className="inline-flex shrink-0 rounded-lg border border-slate-200 bg-slate-100 p-1 dark:border-slate-700 dark:bg-slate-800"
>
{RANGES.map((entry, index) => {
const selected = entry.id === rangeId;
return (
<button
key={entry.id}
ref={(node) => {
tabRefs.current[index] = node;
}}
type="button"
role="tab"
id={`${uid}-tab-${entry.id}`}
aria-selected={selected}
aria-controls={`${uid}-panel`}
tabIndex={selected ? 0 : -1}
onClick={() => selectRange(entry.id)}
onKeyDown={(event) => handleTabKeyDown(event, index)}
className={[
"rounded-md px-3 py-1.5 text-sm font-medium transition-colors outline-none",
"focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-100 dark:focus-visible:ring-offset-slate-800",
selected
? "bg-white text-slate-900 shadow-sm dark:bg-slate-950 dark:text-slate-50"
: "text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-slate-100",
].join(" ")}
>
{entry.label}
</button>
);
})}
</div>
</div>
{/* Legend + compare switch */}
<div className="flex flex-wrap items-center justify-between gap-4">
<div className="flex flex-wrap items-center gap-4 text-xs text-slate-600 dark:text-slate-400">
<span className="inline-flex items-center gap-2">
<span className="h-2 w-2 rounded-full bg-indigo-500" />
This period
</span>
{compare ? (
<span className="inline-flex items-center gap-2">
<svg
viewBox="0 0 16 4"
className="h-1 w-4"
aria-hidden="true"
>
<line
x1="0"
y1="2"
x2="16"
y2="2"
stroke="currentColor"
strokeWidth="2"
strokeDasharray="4 3"
className="text-slate-400 dark:text-slate-500"
/>
</svg>
Previous period
</span>
) : null}
</div>
<button
type="button"
role="switch"
aria-checked={compare}
onClick={() => setCompare((prev) => !prev)}
className="group inline-flex items-center gap-2.5 rounded-md px-1 py-1 text-sm font-medium text-slate-700 outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-slate-300 dark:focus-visible:ring-offset-slate-900"
>
<span
aria-hidden="true"
className={[
"relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors",
compare
? "bg-indigo-600"
: "bg-slate-300 dark:bg-slate-700",
].join(" ")}
>
<span
className={[
"inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform",
compare ? "translate-x-4" : "translate-x-0.5",
].join(" ")}
/>
</span>
Compare to previous
</button>
</div>
</div>
{/* Chart */}
<div
id={`${uid}-panel`}
role="tabpanel"
aria-labelledby={`${uid}-tab-${rangeId}`}
tabIndex={0}
onKeyDown={handleChartKeyDown}
onFocus={() =>
setActiveIndex((prev) => (prev === null ? chart.count - 1 : prev))
}
onBlur={() => setActiveIndex(null)}
aria-describedby={`${uid}-hint`}
className="relative px-2 pt-6 pb-2 outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-inset sm:px-4"
>
<p id={`${uid}-hint`} className="sr-only">
Revenue for the last {range.label}. Use the left and right arrow
keys to move between data points, Home and End to jump to the
first or last, and Escape to clear.
</p>
<div className="relative">
<svg
viewBox={`0 0 ${VB_W} ${VB_H}`}
className="block w-full touch-none"
role="presentation"
onPointerMove={handlePointerMove}
onPointerLeave={() => setActiveIndex(null)}
>
<defs>
<linearGradient
id={`${uid}-fill`}
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="0%"
stopColor="currentColor"
stopOpacity="0.28"
/>
<stop
offset="100%"
stopColor="currentColor"
stopOpacity="0.02"
/>
</linearGradient>
</defs>
{/* Gridlines + y axis */}
{chart.ticks.map((tick) => (
<g key={tick.value}>
<line
x1={PAD_LEFT}
y1={tick.y}
x2={VB_W - PAD_RIGHT}
y2={tick.y}
className="stroke-slate-200 dark:stroke-slate-800"
strokeWidth="1"
/>
<text
x={PAD_LEFT - 12}
y={tick.y}
textAnchor="end"
dominantBaseline="middle"
className="fill-slate-400 text-[11px] tabular-nums dark:fill-slate-500"
>
{tick.value === 0
? "$0"
: moneyCompact.format(tick.value)}
</text>
</g>
))}
{/* X axis labels */}
{chart.points.map((point, index) => {
const show =
index % chart.labelEvery === 0 || index === chart.count - 1;
if (!show) return null;
return (
<text
key={point.full}
x={chart.current[index].x}
y={BASE_Y + 20}
textAnchor="middle"
className="fill-slate-400 text-[11px] dark:fill-slate-500"
>
{point.label}
</text>
);
})}
{/* Previous period */}
{compare ? (
<motion.path
key={`${rangeId}-prev`}
d={chart.prevLine}
fill="none"
strokeWidth="2"
strokeDasharray="5 4"
strokeLinecap="round"
className="stroke-slate-400 dark:stroke-slate-600"
initial={
reduceMotion ? false : { pathLength: 0, opacity: 0 }
}
animate={{ pathLength: 1, opacity: 1 }}
transition={{ duration: 0.7, ease: "easeOut" }}
/>
) : null}
{/* Area */}
<motion.path
key={`${rangeId}-area`}
d={chart.area}
fill={`url(#${uid}-fill)`}
className="text-indigo-500"
initial={reduceMotion ? false : { opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5, ease: "easeOut", delay: 0.1 }}
/>
{/* Line */}
<motion.path
key={`${rangeId}-line`}
d={chart.line}
fill="none"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
className="stroke-indigo-600 dark:stroke-indigo-400"
initial={reduceMotion ? false : { pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 0.9, ease: "easeInOut" }}
/>
{/* Active marker */}
{activeCoord ? (
<g>
<line
x1={activeCoord.x}
y1={PAD_TOP}
x2={activeCoord.x}
y2={BASE_Y}
className="stroke-slate-300 dark:stroke-slate-600"
strokeWidth="1"
strokeDasharray="3 3"
/>
{compare && activeIndex !== null ? (
<circle
cx={chart.previous[activeIndex].x}
cy={chart.previous[activeIndex].y}
r="3.5"
className="fill-white stroke-slate-400 dark:fill-slate-900 dark:stroke-slate-500"
strokeWidth="2"
/>
) : null}
<circle
cx={activeCoord.x}
cy={activeCoord.y}
r="5"
className="fill-white stroke-indigo-600 dark:fill-slate-900 dark:stroke-indigo-400"
strokeWidth="2.5"
/>
</g>
) : null}
</svg>
{/* Tooltip — anchored to the same box as the svg so it tracks the marker */}
{active && activeCoord ? (
<div
aria-hidden="true"
className="pointer-events-none absolute z-10 -translate-x-1/2"
style={{
left: `${tooltipLeft}%`,
top: `${(PAD_TOP / VB_H) * 100}%`,
}}
>
<div className="min-w-[10rem] rounded-lg border border-slate-200 bg-white/95 p-3 shadow-lg backdrop-blur dark:border-slate-700 dark:bg-slate-800/95">
<p className="text-[11px] font-medium text-slate-500 dark:text-slate-400">
{active.full}
</p>
<p className="mt-1 text-base font-semibold tabular-nums text-slate-900 dark:text-slate-50">
{money.format(active.value)}
</p>
{compare ? (
<p className="mt-1 text-xs tabular-nums text-slate-500 dark:text-slate-400">
Previous {money.format(active.prev)} ·{" "}
<span
className={
active.value >= active.prev
? "font-medium text-emerald-600 dark:text-emerald-400"
: "font-medium text-rose-600 dark:text-rose-400"
}
>
{formatDelta(
((active.value - active.prev) / active.prev) * 100,
)}
</span>
</p>
) : null}
</div>
</div>
) : null}
</div>
{/* Announcements + full data for assistive tech */}
<p aria-live="polite" className="sr-only">
{active ? `${active.full}: ${money.format(active.value)}` : ""}
</p>
<table className="sr-only">
<caption>{`Revenue for the last ${range.label}, compared with ${range.comparison}`}</caption>
<thead>
<tr>
<th scope="col">Period</th>
<th scope="col">Revenue</th>
<th scope="col">Previous</th>
</tr>
</thead>
<tbody>
{chart.points.map((point) => (
<tr key={point.full}>
<th scope="row">{point.full}</th>
<td>{money.format(point.value)}</td>
<td>{money.format(point.prev)}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Channel breakdown */}
<div className="border-t border-slate-200 p-6 sm:p-8 dark:border-slate-800">
<div className="flex items-baseline justify-between gap-4">
<h3 className="text-sm font-semibold text-slate-900 dark:text-slate-50">
Where it came from
</h3>
<p className="text-xs tabular-nums text-slate-500 dark:text-slate-400">
Best {range.id === "12m" ? "month" : "day"}: {chart.best.full} ·{" "}
{money.format(chart.best.value)}
</p>
</div>
<ul className="mt-5 grid gap-x-8 gap-y-5 sm:grid-cols-2">
{CHANNELS.map((channel, index) => {
const amount = chart.total * channel.share;
const positive = channel.delta >= 0;
return (
<li key={channel.name}>
<div className="flex items-center justify-between gap-3 text-sm">
<span className="inline-flex items-center gap-2 text-slate-600 dark:text-slate-400">
<span
className={`h-2 w-2 rounded-full ${channel.dot}`}
/>
{channel.name}
</span>
<span className="font-medium tabular-nums text-slate-900 dark:text-slate-100">
{money.format(amount)}
</span>
</div>
<div className="mt-2 flex items-center gap-3">
<div className="h-1.5 flex-1 overflow-hidden rounded-full bg-slate-100 dark:bg-slate-800">
<motion.div
className={`h-full rounded-full ${channel.bar}`}
initial={reduceMotion ? false : { width: 0 }}
animate={{
width: `${(channel.share / CHANNELS[0].share) * 100}%`,
}}
transition={{
duration: 0.7,
ease: "easeOut",
delay: reduceMotion ? 0 : 0.15 + index * 0.08,
}}
/>
</div>
<span
className={[
"w-14 shrink-0 text-right text-xs font-medium tabular-nums",
positive
? "text-emerald-600 dark:text-emerald-400"
: "text-rose-600 dark:text-rose-400",
].join(" ")}
>
{formatDelta(channel.delta)}
</span>
</div>
</li>
);
})}
</ul>
</div>
</div>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Dashboard KPI Row
Originalrow of KPI stat cards with trends

Dashboard Stat Widgets
Originalassorted dashboard stat widgets

Dashboard Activity
Originalrecent activity feed widget

Dashboard Chart Widget
Originalchart panel widget with tabs

Dashboard Users
Originalactive users widget

Dashboard Tasks
Originaltasks/checklist widget

Dashboard Calendar Widget
Originalmini calendar dashboard widget

Dashboard Notifications
Originalnotifications panel widget

Dashboard Overview
Originalcompact dashboard overview grid

Spotlight Hero
OriginalA centred hero with a soft radial spotlight, badge and dual call-to-action.

Split Hero
OriginalA two-column hero pairing a headline and CTAs with a product mock and social proof.

Gradient Spotlight Hero
OriginalA minimal centred hero with a soft gradient-mesh backdrop, announcement pill and dual call-to-action buttons.

