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.
"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>
)
}
pnpm dlx shadcn add @01orbital/sparklinenpx shadcn add @01orbital/sparklineyarn shadcn add @01orbital/sparklinebunx --bun shadcn add @01orbital/sparklineFree 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.
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>
)
}Prop
Type
It also accepts the HTML attributes of the element it renders.
npm: cn. The CLI installs them for you.
No other registry pieces.
Scratch Reveal
Teal painted panel whose paint wears away on hover to expose a pencil construction sketch, with the label switching to canvas ink. Set in Marcellus; decorative, and reduced motion skips the fade.
Transit Light Curve
An SVG light curve in currentColor with a dot riding its dip, driven by the same clock as Planet Transit Scene. Render both from one parent so the dip matches the crossing.