# Stat Card Area

> Revenue stat card with a gradient area sparkline, hover-to-read values, NumberFlow, and trend badge

A shadcn/ui block from the [@bklit](https://ui.bklit.com/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/bklit/stat-card-area-01).

- Page, with a live preview: https://shadcn.exchange/r/bklit/stat-card-area-01
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/bklit/stat-card-area-01-light-231ae1a92d1eaebb.png
- Registry item: https://ui.bklit.com/r/stat-card-area-01.json
- Listed: 2026-10-13, source last changed 2026-10-26

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/bklit/stat-card-area-01.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `@number-flow/react`
- Registry items: `@bklit/sparkline`, `@bklit/chart-stat-flow`, `card`, `badge`

## Files

### `registry/blocks/stat-card-area-01/components/stat-card-area.tsx`

```tsx
"use client";

import { AreaSparkline, ChartStatFlow } from "@/components/charts";
import {
  Card,
  CardAction,
  CardContent,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import { revenueSeries, revenueStats } from "../data/revenue-series";
import {
  StatCardChart,
  statCardLabelClassName,
  statCardValueClassName,
} from "./stat-card-chart";
import {
  formatStatCardMonth,
  useStatCardSparklineHover,
} from "./stat-card-sparkline-hover";
import { TrendBadge } from "./trend-badge";

const revenueValues = revenueSeries.map((point) => point.value);
const revenueMin = Math.min(...revenueValues);
const revenueMax = Math.max(...revenueValues);
// 5% headroom above and below so peaks and dips don't touch the card edges.
const revenueDomain: [number, number] = [
  revenueMin - (revenueMax - revenueMin) * 0.05,
  revenueMax + (revenueMax - revenueMin) * 0.05,
];

export function StatCardArea() {
  const { hover, sparklineProps } = useStatCardSparklineHover(
    revenueSeries,
    formatStatCardMonth
  );
  const displayValue = hover.value ?? revenueStats.average;
  const displayLabel = hover.label ?? "Avg";
  const displayTrend = hover.trend ?? revenueStats.trend;

  return (
    <Card className="w-full gap-0 py-0">
      <CardHeader className="px-4 py-3">
        <CardTitle>Total Revenue</CardTitle>
        <CardAction>
          <TrendBadge value={displayTrend} />
        </CardAction>
      </CardHeader>

      <CardContent className="flex flex-col gap-3 px-4 pt-2 pb-3">
        <ChartStatFlow
          formatOptions={{
            currency: "USD",
            maximumFractionDigits: 0,
            style: "currency",
          }}
          label={displayLabel}
          labelClassName={statCardLabelClassName}
          value={displayValue}
          valueClassName={statCardValueClassName}
        />

        <StatCardChart size="md">
          <AreaSparkline
            color="var(--chart-1)"
            crosshair="gradient"
            data={revenueSeries}
            dataKey="value"
            fillOpacity={0.45}
            height={190}
            padding={0}
            strokeWidth={2}
            yDomain={revenueDomain}
            {...sparklineProps}
          />
        </StatCardChart>
      </CardContent>
    </Card>
  );
}
```

### `registry/blocks/stat-card-area-01/components/stat-card-chart.tsx`

```tsx
"use client";

import type { ReactNode } from "react";
import { cn } from "@/lib/utils";

export interface StatCardHoverState {
  value: number | null;
  label: string | null;
  trend: number | null;
}

export const statCardValueClassName =
  "text-3xl font-semibold leading-none tracking-tight";

export const statCardLabelClassName = "mt-0 text-xs";

export const statCardChartHeights = {
  sm: "[--stat-card-chart-h:96px]",
  md: "[--stat-card-chart-h:190px]",
  lg: "[--stat-card-chart-h:420px]",
} as const;

/** Bleeds charts edge-to-edge inside stat card content padding. */
export function StatCardChart({
  children,
  className,
  size = "sm",
}: {
  children: ReactNode;
  className?: string;
  size?: keyof typeof statCardChartHeights;
}) {
  return (
    <div
      className={cn(
        "relative -mx-4 -mb-3 overflow-hidden",
        "[&_.relative.w-full]:aspect-auto! [&_.relative.w-full]:h-[var(--stat-card-chart-h)]!",
        statCardChartHeights[size],
        className
      )}
    >
      {children}
    </div>
  );
}
```

### `registry/blocks/stat-card-area-01/components/stat-card-sparkline-hover.ts`

```ts
"use client";

import { useSparklineHover } from "@/components/charts";
import type { StatCardHoverState } from "./stat-card-chart";

export type { StatCardHoverState } from "./stat-card-chart";

export interface StatCardPoint {
  date: Date;
  value: number;
}

export function formatStatCardMonth(date: Date) {
  return date.toLocaleDateString("en-US", { month: "short" });
}

export function formatStatCardWeekday(date: Date) {
  return date.toLocaleDateString("en-US", { weekday: "long" });
}

/** Percent change from the previous point, or `null` for the first point. */
function computePeriodTrend(
  data: readonly StatCardPoint[],
  index: number
): number | null {
  const current = data[index]?.value;
  const previous = data[index - 1]?.value;
  if (current === undefined || previous === undefined || previous === 0) {
    return null;
  }
  return ((current - previous) / previous) * 100;
}

/**
 * Tracks the hovered sparkline point and derives the stat card's value,
 * label and period-over-period trend from it. Spread `sparklineProps`
 * onto the sparkline.
 */
export function useStatCardSparklineHover(
  data: readonly StatCardPoint[],
  formatLabel: (date: Date) => string
) {
  const { index, datum, sparklineProps } = useSparklineHover<StatCardPoint>();

  const hover: StatCardHoverState =
    index === null || !datum
      ? { value: null, label: null, trend: null }
      : {
          value: datum.value,
          label: formatLabel(datum.date),
          trend: computePeriodTrend(data, index),
        };

  return { hover, sparklineProps };
}
```

### `registry/blocks/stat-card-area-01/components/trend-badge.tsx`

```tsx
"use client";

import type { SVGProps } from "react";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";

/** Central arrow icon (round join, outlined, radius 0, stroke 1.5), inlined. */
function TrendArrow({
  direction,
  ...props
}: SVGProps<SVGSVGElement> & { direction: "up" | "down" }) {
  return (
    <svg
      {...props}
      aria-hidden
      fill="none"
      height="24px"
      viewBox="0 0 24 24"
      width="24px"
      xmlns="http://www.w3.org/2000/svg"
    >
      {direction === "up" ? (
        <>
          <path
            d="M12 20.25V4.5"
            stroke="currentColor"
            strokeLinecap="round"
            strokeLinejoin="round"
            strokeWidth="1.5"
          />
          <path
            d="M5.75 10L12 3.75L18.25 10"
            stroke="currentColor"
            strokeLinecap="round"
            strokeLinejoin="round"
            strokeWidth="1.5"
          />
        </>
      ) : (
        <>
          <path
            d="M18.25 14L12 20.25L5.75 14"
            stroke="currentColor"
            strokeLinecap="round"
            strokeLinejoin="round"
            strokeWidth="1.5"
          />
          <path
            d="M12 19.5V3.75"
            stroke="currentColor"
            strokeLinecap="round"
            strokeLinejoin="round"
            strokeWidth="1.5"
          />
        </>
      )}
    </svg>
  );
}

export function TrendBadge({
  value,
  className,
}: {
  value: number;
  className?: string;
}) {
  const positive = value >= 0;

  return (
    <Badge
      className={cn(
        positive &&
          "border-emerald-500/20 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400",
        className
      )}
      variant={positive ? "outline" : "destructive"}
    >
      <TrendArrow
        className="size-3"
        data-icon="inline-start"
        direction={positive ? "up" : "down"}
      />
      {positive ? "+" : ""}
      {value.toFixed(1)}%
    </Badge>
  );
}
```

### `registry/blocks/stat-card-area-01/data/revenue-series.ts`

```ts
export const revenueSeries = [
  { date: new Date("2024-01-01"), value: 5200 },
  { date: new Date("2024-02-01"), value: 6100 },
  { date: new Date("2024-03-01"), value: 5400 },
  { date: new Date("2024-04-01"), value: 4700 },
  { date: new Date("2024-05-01"), value: 5100 },
  { date: new Date("2024-06-01"), value: 6800 },
  { date: new Date("2024-07-01"), value: 6400 },
  { date: new Date("2024-08-01"), value: 7200 },
  { date: new Date("2024-09-01"), value: 6900 },
  { date: new Date("2024-10-01"), value: 8100 },
  { date: new Date("2024-11-01"), value: 7600 },
  { date: new Date("2024-12-01"), value: 8800 },
];

export const revenueStats = {
  average:
    revenueSeries.reduce((sum, point) => sum + point.value, 0) /
    revenueSeries.length,
  trend: 12.4,
};
```

### `registry/examples/stat-card-area-01-index.ts`

```ts
/** biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install */
export { AreaSparkline, useSparklineHover } from "./sparkline";
export { ChartStatFlow } from "./chart-stat-flow";
```
