DHxWhy

Breakdown Stat Card – KPI Card that Splits a Number into Animated Segments

Cards

About

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>
  );
}

Dependencies

More by DHxWhy

View all
  • Product Rail Gallery – Fashion Store Hero Section

    Product Rail Gallery – Fashion Store Hero Section

    2.2K
  • Sushi Conveyor Belt Menu – Restaurant Hero Section

    Sushi Conveyor Belt Menu – Restaurant Hero Section

    883
  • Team Lanyard Rail – Meet the Team Section with Flip Badges

    Team Lanyard Rail – Meet the Team Section with Flip Badges

    827
  • Skateboard Deck Rail Gallery – Skate Shop Hero Section

    Skateboard Deck Rail Gallery – Skate Shop Hero Section

    430
  • Sun Moon Dark Mode Theme Toggle

    Sun Moon Dark Mode Theme Toggle

    328
  • Picture Rail Image Gallery – Art Print Shop Hero Section

    Picture Rail Image Gallery – Art Print Shop Hero Section

    268
  • Pendant Lamp Rail Gallery – Lighting Store Hero Section

    Pendant Lamp Rail Gallery – Lighting Store Hero Section

    243
  • World Testimonials Globe – Social Proof Hero Section

    World Testimonials Globe – Social Proof Hero Section

    217

More in Cards

View all
  • Course Design Cards

    Course Design Cards

    1.2K
  • Interactive Tech Stack Bento Grid with Hover Effects

    Interactive Tech Stack Bento Grid with Hover Effects

    389
  • Stats cards with links

    Stats cards with links

    128
  • Card Studio

    Card Studio

    14
  • Statistics Card 2

    Statistics Card 2

    291
  • Statistics Card 10

    Statistics Card 10

    280
  • Card

    Card

    70
  • The Item one

    The Item one

    3