# Sparkline

> Tiny responsive SVG line/area chart with gradient fill, last-point dot and up/down trend colors.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/components/sparkline
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/sparkline.json
```

Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Dependencies

No extra dependencies.

## Usage

```tsx title="components/sparkline-demo.tsx"
import { Sparkline } from "@/components/sparkline"

const rows = [
  { label: "Revenue", value: "$48.2k", trend: "auto" as const, data: [12, 14, 13, 18, 17, 22, 21, 27, 26, 31, 34, 38] },
  { label: "Churn", value: "2.4%", trend: "auto" as const, data: [9, 8.5, 9.2, 8, 7.4, 7.9, 6.1, 5.8, 5.2, 4.1, 3.6, 2.4] },
  { label: "Sessions", value: "12.9k", trend: "primary" as const, data: [30, 28, 35, 31, 40, 36, 44, 39, 47, 42, 49, 45] },
  { label: "Flat", value: "0.0%", trend: "primary" as const, data: [5, 5, 5, 5, 5, 5] },
]

export default function SparklineDemo() {
  return (
    <div className="grid w-full max-w-md gap-6">
      <div className="divide-y rounded-2xl border bg-card px-4">
        {rows.map((row) => (
          <div key={row.label} className="flex items-center justify-between gap-6 py-4">
            <div className="min-w-0">
              <p className="text-sm text-muted-foreground">{row.label}</p>
              <p className="text-xl font-semibold tabular-nums">{row.value}</p>
            </div>
            <Sparkline data={row.data} trend={row.trend} className="h-10 w-36" label={`${row.label} trend`} />
          </div>
        ))}
      </div>
      <div className="grid gap-2">
        <p className="text-sm font-medium text-muted-foreground">Responsive width, line only</p>
        <Sparkline data={[3, 7, 4, 9, 6, 12, 8, 14]} area={false} className="h-16" />
      </div>
    </div>
  )
}
```

## Source

```tsx title="components/sparkline.tsx"
import * as React from "react"

import { cn } from "@/lib/utils"

export interface SparklineProps extends Omit<React.ComponentProps<"div">, "children"> {
  data: number[]
  /** "auto" colours by first-vs-last value; "primary" uses the theme primary colour */
  trend?: "auto" | "up" | "down" | "primary"
  /** Draw the gradient area under the line */
  area?: boolean
  /** Draw a dot on the last point */
  showDot?: boolean
  strokeWidth?: number
  /** Accessible description; defaults to an automatic summary */
  label?: string
}

const toneClass = {
  up: "text-emerald-500 dark:text-emerald-400",
  down: "text-rose-500 dark:text-rose-400",
  primary: "text-primary",
} as const

export function Sparkline({
  data,
  trend = "auto",
  area = true,
  showDot = true,
  strokeWidth = 2,
  label,
  className,
  ...props
}: SparklineProps) {
  const id = React.useId().replace(/[^\w-]/g, "")
  const n = data.length

  if (n === 0) {
    return <div className={cn("h-10 w-full", className)} {...props} />
  }

  const min = Math.min(...data)
  const max = Math.max(...data)
  const span = max - min
  // Plot inside a padded 0-100 box so the stroke and dot are never clipped.
  const points = data.map((d, i) => ({
    x: n === 1 ? 50 : 3 + (i / (n - 1)) * 94,
    y: span === 0 ? 50 : 90 - ((d - min) / span) * 80,
  }))
  const line = points.map((p, i) => `${i === 0 ? "M" : "L"}${p.x.toFixed(2)},${p.y.toFixed(2)}`).join(" ")
  const last = points[n - 1]
  const areaPath = `${line} L${last.x.toFixed(2)},100 L${points[0].x.toFixed(2)},100 Z`

  const resolved = trend === "auto" ? (data[n - 1] >= data[0] ? "up" : "down") : trend
  const summary = label ?? `Trend across ${n} points, from ${data[0]} to ${data[n - 1]}`

  return (
    <div
      role="img"
      aria-label={summary}
      className={cn("relative h-10 w-full", toneClass[resolved], className)}
      {...props}
    >
      <svg viewBox="0 0 100 100" preserveAspectRatio="none" className="absolute inset-0 size-full overflow-visible" aria-hidden>
        <defs>
          <linearGradient id={id} x1="0" x2="0" y1="0" y2="1">
            <stop offset="0%" stopColor="currentColor" stopOpacity={0.3} />
            <stop offset="100%" stopColor="currentColor" stopOpacity={0} />
          </linearGradient>
        </defs>
        {area && n > 1 && <path d={areaPath} fill={`url(#${id})`} />}
        {n > 1 && (
          <path
            d={line}
            fill="none"
            stroke="currentColor"
            strokeWidth={strokeWidth}
            strokeLinecap="round"
            strokeLinejoin="round"
            vectorEffect="non-scaling-stroke"
          />
        )}
      </svg>
      {showDot && (
        <span
          aria-hidden
          className="absolute size-2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-current ring-2 ring-background"
          style={{ left: `${last.x}%`, top: `${last.y}%` }}
        />
      )}
    </div>
  )
}
```
