Skip to content
goodtabs UI
BrowseBrowse components

Sparkline stat

A stat with a plain SVG sparkline, two date labels and one brand mark on the last point.

npx shadcn@latest add https://ui.goodtabs.co/r/sparkline-stat.json

With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/sparkline-stat

Weekly installs

4,218

A rising series with its first and last date.

Falling and flat

Median load

1.1s

Open issues

37

Each line scales to its own range, so shape is what you read.

Usage

import { SparklineStat } from "@/components/ui/sparkline-stat"

<SparklineStat label="Weekly installs" value="4,218" data={[310, 340, 420, 560]} from="Aug 18" to="Nov 3" />

Props

PropTypeDefaultDescription
labelstringWhat the number measures.
valueReactNodeThe number, already formatted.
datanumber[]The series, oldest first. At least two points.
fromstringLabel under the left end of the line.
tostringLabel under the right end of the line.
unitstring""Unit word used in the text alternative.

Source

sparkline-stat/sparkline-stat.tsx
import * as React from "react";
import { cn } from "@/lib/utils";

type SparklineStatProps = Omit<React.ComponentProps<"div">, "children"> & {
  /** What the number measures. */
  label: string;
  /** The number, already formatted. */
  value: React.ReactNode;
  /** The series, oldest first. Needs at least two points. */
  data: number[];
  /** Label for the first point, for example "Oct 6". */
  from: string;
  /** Label for the last point, for example "Nov 5". */
  to: string;
  /** Unit word used in the text alternative, for example "visits". */
  unit?: string;
};

const W = 100;
const H = 40;
const PAD = 3;

/**
 * A stat with a plain SVG sparkline. The series is scaled to its own min and max, so the line shows shape, not size.
 * The last point is the one brand mark.
 */
export function SparklineStat({ label, value, data, from, to, unit = "", className, ...props }: SparklineStatProps) {
  const min = Math.min(...data);
  const max = Math.max(...data);
  const span = max - min || 1;
  const last = data.length - 1;
  const x = (i: number) => (i / last) * W;
  const y = (v: number) => H - PAD - ((v - min) / span) * (H - PAD * 2);
  const points = data.map((v, i) => `${x(i).toFixed(2)},${y(v).toFixed(2)}`);
  const line = `M${points.join("L")}`;
  const area = `${line}L${W},${H}L0,${H}Z`;
  const text = `${label}: from ${data[0]} to ${data[last]}${unit ? ` ${unit}` : ""} between ${from} and ${to}, low ${min}, high ${max}.`;

  return (
    <div
      data-slot="sparkline-stat"
      className={cn("flex w-full flex-col gap-3 rounded-lg border bg-background p-4", className)}
      {...props}
    >
      <div className="flex flex-col gap-1">
        <p className="m-0 text-sm text-muted-foreground">{label}</p>
        <p className="m-0 text-2xl font-medium tracking-tight tabular-nums">{value}</p>
      </div>
      <div className="relative h-10 w-full">
        <svg
          role="img"
          aria-label={text}
          viewBox={`0 0 ${W} ${H}`}
          preserveAspectRatio="none"
          className="absolute inset-0 size-full overflow-visible text-foreground"
        >
          <line
            x1="0"
            x2={W}
            y1={H - 0.5}
            y2={H - 0.5}
            stroke="currentColor"
            strokeOpacity="0.14"
            vectorEffect="non-scaling-stroke"
          />
          <path d={area} fill="currentColor" fillOpacity="0.07" />
          <path
            d={line}
            fill="none"
            stroke="currentColor"
            strokeOpacity="0.8"
            strokeWidth="1.5"
            strokeLinejoin="round"
            strokeLinecap="round"
            vectorEffect="non-scaling-stroke"
          />
        </svg>
        <span
          aria-hidden="true"
          className="absolute size-2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-brand ring-2 ring-background"
          style={{ left: "100%", top: `${(y(data[last]) / H) * 100}%` }}
        />
      </div>
      <div aria-hidden="true" className="flex justify-between font-mono text-xs text-muted-foreground">
        <span>{from}</span>
        <span>{to}</span>
      </div>
    </div>
  );
}

For AI agents

Hand your agent this page, the markdown at /components/sparkline-stat/prompt.md, or the Copy AI prompt button above.

When to use

  • A headline number where the shape of the last few weeks matters
  • Dashboards that need a trend without axes and gridlines

When not to use

  • Reading exact values: use a full chart with axes
  • Comparing several series on one scale

Agent notes

  • Server component. The SVG has role img and an aria-label with the first, last, low and high values
  • Each series is scaled to its own min and max, so two sparklines are not comparable by height
  • Stretches to the width of its container; the line uses non-scaling strokes so it stays 1.5px
  • The brand dot on the last point is the only coloured mark

The maths

  • A point's x is i / (n - 1) × 100 and its y is 40 - 3 - (v - min) / (max - min) × 34, so the series fills the box with 3 units of padding.
  • The area is the line closed along the baseline at 7 percent foreground, so the 1.5px stroke stays the loudest mark.