Graphics

Sparkline

Compact SVG line chart from an array of numbers, with a currentColor stroke and fading area fill. Missing values break the line, one point draws a dot, and an aria-label exposes it as an image.

Free, MIT licenseOpen in Browse
"use client"

import { useId } from "react"
import { cn } from "cn"

// Stroke width in px; the plot is inset by half of it so peaks aren't clipped.
const STROKE = 2

/**
 * A small line chart of `data` with a fading fill beneath it, drawn in
 * `currentColor`, for a trend beside a stat or in a table row.
 *
 * Hidden from assistive tech unless you pass `aria-label` or
 * `aria-labelledby`, which give it `role="img"`. The drawing stretches to the
 * size you set with `className`; the line stays 2px wide.
 */
export function Sparkline({
  data,
  width = 200,
  height = 60,
  className,
  ...props
}: Omit<React.ComponentProps<"svg">, "width" | "height" | "children"> & {
  /** Values to plot, left to right; non-finite entries leave a gap. */
  data: number[]
  /** Width of the `viewBox`. Set the rendered size with `className`. */
  width?: number
  /** Height of the `viewBox`. Set the rendered size with `className`. */
  height?: number
}) {
  const fillId = useId()
  const values = data.filter(Number.isFinite)
  const max = Math.max(...values)
  const min = Math.min(...values)
  const range = max - min || 1
  const pad = STROKE / 2
  const plot = height - STROKE
  const step = data.length > 1 ? width / (data.length - 1) : 0
  const pts = data.flatMap((v, i) =>
    Number.isFinite(v)
      ? [
          {
            i,
            x: values.length === 1 ? width / 2 : i * step,
            y:
              max === min
                ? height / 2
                : pad + plot - ((v - min) / range) * plot,
          },
        ]
      : []
  )
  // A missing sample breaks the line; it never pulls later points leftward.
  const runs = pts.reduce<(typeof pts)[]>((acc, p, n) => {
    if (n && pts[n - 1].i === p.i - 1) acc[acc.length - 1].push(p)
    else acc.push([p])
    return acc
  }, [])
  const trace = (run: typeof pts) =>
    run.map((p, n) => `${n ? "L" : "M"}${p.x},${p.y}`).join(" ") +
    (run.length === 1 && pts.length > 1 ? "h0" : "")
  const line = runs.map(trace).join(" ")
  const area = runs
    .filter((run) => run.length > 1)
    .map(
      (run) =>
        `${trace(run)} L${run[run.length - 1].x},${height} L${run[0].x},${height} Z`
    )
    .join(" ")
  const labelled = Boolean(props["aria-label"] || props["aria-labelledby"])

  return (
    <svg
      data-slot="sparkline"
      viewBox={`0 0 ${width} ${height}`}
      preserveAspectRatio="none"
      role={labelled ? "img" : undefined}
      aria-hidden={labelled ? undefined : true}
      className={cn("overflow-visible", className)}
      {...props}
    >
      <defs>
        <linearGradient id={fillId} x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stopColor="currentColor" stopOpacity="0.25" />
          <stop offset="100%" stopColor="currentColor" stopOpacity="0" />
        </linearGradient>
      </defs>
      {area && <path d={area} fill={`url(#${fillId})`} />}
      {pts.length === 1 && (
        <path
          d={`M${pts[0].x},${pts[0].y}h0`}
          stroke="currentColor"
          strokeWidth={STROKE * 2}
          strokeLinecap="round"
          vectorEffect="non-scaling-stroke"
        />
      )}
      <path
        d={line}
        fill="none"
        stroke="currentColor"
        strokeWidth={STROKE}
        strokeLinecap="round"
        strokeLinejoin="round"
        vectorEffect="non-scaling-stroke"
      />
    </svg>
  )
}

Installation

pnpm dlx shadcn add @01orbital/sparkline
npx shadcn add @01orbital/sparkline
yarn shadcn add @01orbital/sparkline
bunx --bun shadcn add @01orbital/sparkline

Free under the MIT license. Add the @01orbital registry to components.json once (install guide), then run the command.

It lands at components/graphics/sparkline.tsx, as source you own and edit.

Usage

import { Sparkline } from "@/components/graphics/sparkline"

export default function SparklineDemo() {
  const data = [8, 10, 9, 13, 11, 16, 14, 18, 15, 21, 19, 24]
  return (
    <div className="grid text-primary">
      <Sparkline
        data={data}
        width={260}
        height={80}
        aria-label="Signups rising over 12 weeks, from 8 to 24"
        className="w-72"
      />
    </div>
  )
}

API reference

Prop

Type

It also accepts the HTML attributes of the element it renders.

Dependencies

npm: cn. The CLI installs them for you.

No other registry pieces.