# Stat Card Line

> Sessions stat card with a line sparkline, hover-to-read values, overlaid 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-line-01).

- Page, with a live preview: https://shadcn.exchange/r/bklit/stat-card-line-01
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/bklit/stat-card-line-01-light-102c475f3645cd2e.png
- Registry item: https://ui.bklit.com/r/stat-card-line-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-line-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-line-01/components/stat-card-line.tsx`

```tsx
"use client";

import { ChartStatFlow, LineSparkline } from "@/components/charts";
import {
  Card,
  CardAction,
  CardContent,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import { sessionsSeries, sessionsStats } from "../data/sessions-series";
import {
  StatCardChart,
  statCardLabelClassName,
  statCardValueClassName,
} from "./stat-card-chart";
import {
  formatStatCardWeekday,
  useStatCardSparklineHover,
} from "./stat-card-sparkline-hover";
import { TrendBadge } from "./trend-badge";

const sessionValues = sessionsSeries.map((point) => point.value);
const sessionMin = Math.min(...sessionValues);
const sessionMax = Math.max(...sessionValues);
// 5% headroom above and below so the stroke doesn't clip at the card edges.
const sessionDomain: [number, number] = [
  sessionMin - (sessionMax - sessionMin) * 0.05,
  sessionMax + (sessionMax - sessionMin) * 0.05,
];

export function StatCardLine() {
  const { hover, sparklineProps } = useStatCardSparklineHover(
    sessionsSeries,
    formatStatCardWeekday
  );
  const average = Math.round(sessionsStats.average);
  const displayValue = hover.value === null ? average : Math.round(hover.value);
  const displayLabel = hover.label ?? "Avg";
  const displayTrend = hover.trend ?? sessionsStats.trend;

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

      <CardContent className="px-4 pt-2 pb-3">
        <StatCardChart size="md">
          <div className="pointer-events-none absolute right-4 bottom-4 z-10 flex flex-col items-end text-right">
            <ChartStatFlow
              label={displayLabel}
              labelClassName={statCardLabelClassName}
              value={displayValue}
              valueClassName={statCardValueClassName}
            />
          </div>

          <LineSparkline
            color="var(--chart-3)"
            crosshair="gradient"
            curve="monotone"
            data={sessionsSeries}
            dataKey="value"
            height={190}
            padding={0}
            strokeWidth={2.5}
            yDomain={sessionDomain}
            {...sparklineProps}
          />
        </StatCardChart>
      </CardContent>
    </Card>
  );
}
```

### `registry/blocks/stat-card-line-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-line-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-line-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-line-01/data/sessions-series.ts`

```ts
export const sessionsSeries = [
  { date: new Date("2024-06-03"), value: 920 },
  { date: new Date("2024-06-04"), value: 1380 },
  { date: new Date("2024-06-05"), value: 1120 },
  { date: new Date("2024-06-06"), value: 1580 },
  { date: new Date("2024-06-07"), value: 1240 },
  { date: new Date("2024-06-08"), value: 1710 },
  { date: new Date("2024-06-09"), value: 1460 },
];

export const sessionsStats = {
  average:
    sessionsSeries.reduce((sum, point) => sum + point.value, 0) /
    sessionsSeries.length,
  trend: 8.2,
};
```

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

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