Breakdown Stat Card for React and Tailwind CSS (shadcn/ui): a KPI tile whose big number opens like a drawer into its segments. The bar grows from the left, amounts roll up from zero, and hovering or focusing a segment rolls the headline value to its amount. It is built to show what a metric is made of without leaving the dashboard. Shares always sum to 100; expanded state and highlighted segment are controlled or uncontrolled. Roving-tabindex keyboard, polite announcements, light and dark, instant reduced motion. Use it for revenue, users, traffic or cost tiles. The included demo scripts two Alder tiles and makes no requests. Requires React only, with no animation library.
Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Breakdown Stat Card – KPI Card that Splits a Number into Animated Segments to your project and fits it to your theme.
Code
How Breakdown Stat Card – KPI Card that Splits a Number into Animated Segments is used: the demo's code. The component's own source comes with a free account.
Usage.tsx
"use client";
import * as React from "react";
import { BreakdownStatCard } from "@/components/ui/breakdown-stat-card";
import type { BreakdownDelta, BreakdownFormat, BreakdownSegment } from "@/components/ui/breakdown-stat-card";
type CardData = { label: string; period: string; value: number; format?: BreakdownFormat; delta: BreakdownDelta; segments: BreakdownSegment[] };
type Pair = [string | null, string | null];
type Scene = { open: [boolean, boolean]; highlight: Pair };
type Step = { at: number; apply: (scene: Scene) => Scene };
const REVENUE: CardData = {
label: "Monthly revenue",
period: "Oct 2026",
value: 128400,
format: { style: "currency", currency: "USD" },
delta: { value: 8.2, label: "vs Sep" },
segments: [
{ id: "subscriptions", label: "Subscriptions", value: 87300 },
{ id: "services", label: "Services", value: 28200 },
{ id: "add-ons", label: "Add-ons", value: 12900 },
],
};
const USERS: CardData = {
label: "Active users",
period: "This week",
value: 24816,
delta: { value: 3.1, label: "vs last week" },
segments: [
{ id: "web", label: "Web", value: 14920 },
{ id: "ios", label: "iOS", value: 6410 },
{ id: "android", label: "Android", value: 3486 },
],
};
const TICKETS: CardData = {
label: "Support tickets",
period: "This week",
value: 1236,
delta: { value: 12.4, label: "vs last week", goodDirection: "down" },
segments: [
{ id: "email", label: "Email", value: 412 },
{ id: "chat", label: "Chat", value: 412 },
{ id: "phone", label: "Phone", value: 412 },
],
};
const SPEND: CardData = {
label: "Cloud spend",
period: "Nov to date",
value: 10000,
format: { style: "currency", currency: "USD" },
delta: { value: -4.1, label: "vs Oct", goodDirection: "down" },
segments: [
{ id: "compute", label: "Compute", value: 6000, color: "chart-4" },
{ id: "storage", label: "Storage", value: 2000, color: "chart-5" },
],
};
const CYCLE = 9000;
const IDLE: Scene = { open: [false, false], highlight: [null, null] };
const STEPS: Step[] = [
{ at: 800, apply: (scene) => ({ ...scene, open: [true, scene.open[1]] }) },
{ at: 2400, apply: (scene) => ({ ...scene, highlight: ["services", scene.highlight[1]] }) },
{ at: 3600, apply: (scene) => ({ ...scene, highlight: [null, scene.highlight[1]] }) },
{ at: 4200, apply: (scene) => ({ ...scene, open: [scene.open[0], true] }) },
{ at: 5400, apply: (scene) => ({ ...scene, highlight: [scene.highlight[0], "ios"] }) },
{ at: 5600, apply: (scene) => ({ ...scene, open: [false, scene.open[1]] }) },
{ at: 6400, apply: (scene) => ({ ...scene, highlight: [scene.highlight[0], null] }) },
{ at: 7600, apply: (scene) => ({ ...scene, open: [scene.open[0], false] }) },
];
const EASE = "cubic-bezier(0.22, 1, 0.36, 1)";
export default function Demo({ autoplay = true }: { autoplay?: boolean }) {
const params = React.useMemo(() => new URLSearchParams(typeof window === "undefined" ? "" : window.location.search), []);
const fixture = params.get("fixture");
const second = fixture === "thirds" ? TICKETS : fixture === "mismatch" ? SPEND : USERS;
const lock = params.get("lock");
const startExpanded = params.get("expanded") === "1";
const scripted = autoplay && lock === null && !startExpanded;
const [scene, setScene] = React.useState(IDLE);
const [stopped, setStopped] = React.useState(false);
const [userHover, setUserHover] = React.useState([null, null] as Pair);
const [requests, setRequests] = React.useState([] as string[]);
const [hovered, setHovered] = React.useState("");
const [selected, setSelected] = React.useState("");
const [motion, setMotion] = React.useState({ ready: false, reduced: true });
const [entered, setEntered] = React.useState(!scripted);
const stageRef = React.useRef(null as HTMLDivElement | null);
React.useEffect(() => {
const media = window.matchMedia("(prefers-reduced-motion: reduce)");
const update = () => setMotion({ ready: true, reduced: media.matches });
update();
media.addEventListener("change", update);
return () => media.removeEventListener("change", update);
}, []);
React.useEffect(() => {
if (!motion.ready || !scripted) return;
const node = stageRef.current;
if (!motion.reduced && node && typeof node.animate === "function") {
node.animate([{ opacity: 0, transform: "translateY(10px)" }, { opacity: 1, transform: "translateY(0)" }], { duration: 420, easing: EASE });
}
setEntered(true);
}, [motion.ready, motion.reduced, scripted]);
React.useEffect(() => {
if (!scripted || stopped) return;
const start = performance.now();
let index = 0;
let cycle = 0;
let timer = 0;
const schedule = () => {
const step = STEPS[index];
const at = cycle * CYCLE + (step ? step.at : CYCLE);
timer = window.setTimeout(() => {
if (step) {
setScene(step.apply);
index += 1;
} else {
setScene(IDLE);
index = 0;
cycle += 1;
}
schedule();
}, Math.max(0, at - (performance.now() - start)));
};
schedule();
return () => window.clearTimeout(timer);
}, [scripted, stopped]);
const stop = () => { if (scripted && !stopped) setStopped(true); };
const record = (next: boolean) => setRequests((current) => [...current, String(next)]);
const propsFor = (index: 0 | 1) => {
const shared = {
onSegmentSelect: (id: string) => setSelected(id),
onSegmentHover: (id: string | null) => {
setHovered(id ?? "null");
setUserHover((current) => { const next: Pair = [current[0], current[1]]; next[index] = id; return next; });
},
};
if (scripted) {
return {
...shared,
expanded: scene.open[index],
onExpandedChange: (next: boolean) => {
stop();
setScene((current) => { const open: [boolean, boolean] = [current.open[0], current.open[1]]; open[index] = next; return { ...current, open }; });
record(next);
},
...(stopped ? {} : { highlightedId: userHover[index] ?? scene.highlight[index] }),
};
}
if (index === 0 && lock !== null) return { ...shared, expanded: lock === "open", onExpandedChange: record };
return { ...shared, defaultExpanded: startExpanded, onExpandedChange: record };
};
return (
<main
data-demo-root
data-stopped={stopped ? "true" : "false"}
data-requests={requests.join(" ")}
data-hovered={hovered}
data-selected={selected}
onPointerDownCapture={stop}
onKeyDownCapture={stop}
className="flex w-full items-center justify-center overflow-hidden bg-background px-6 text-foreground"
style={{ height: "min(100svh, 760px)" }}
>
<div ref={stageRef} data-stage className="h-[530px] w-full max-w-[1000px] sm:h-[266px]" style={{ opacity: entered ? 1 : 0 }}>
<div className="grid items-start gap-4 sm:grid-cols-2">
<BreakdownStatCard {...REVENUE} {...propsFor(0)} />
<BreakdownStatCard {...second} {...propsFor(1)} />
</div>
</div>
</main>
);
}
















