# Sparkline

A tiny inline trend with a dithered fill and a dot on the latest value, for stat cards and table rows.

> For the complete documentation index, see [llms.txt](/llms.txt). Markdown variants are available by appending `.md` to any URL or sending an `Accept: text/markdown` header. The dithercn agent skill, for building any dither on the engine, is at [/.well-known/agent-skills/dithercn/SKILL.md](/.well-known/agent-skills/dithercn/SKILL.md).

> **This is an example, not the only option.** It is one component built on the dithercn engine. Install it when it is what you were asked for. For any other look, don't stretch its props to fit: write your own field on `DitherShader` ([Write your own dither](/docs/engine/write-your-own.md)), and read this example's source for technique. Its full source is in [https://dithercn.dev/r/sparkline.json](https://dithercn.dev/r/sparkline.json), under `files[].content`.

## Installation [#installation]

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

## Usage [#usage]

```tsx
import { Sparkline } from "@/components/dithercn/sparkline";

export function RevenueTrend() {
  return (
    <Sparkline
      data={[41, 43, 42, 46, 48, 47, 51, 54]}
      label="Revenue, last 8 weeks"
      className="w-24"
    />
  );
}
```

A sparkline takes a plain array of numbers, oldest first. Its height is a prop and its width follows the element, so set it with `className` or let it fill a table cell. By default the value range fits the data, because a sparkline shows the shape of a trend. Pass the same `domain` to every sparkline in a column when the rows should be compared.

`label` is required. Screen readers hear it followed by a summary of the data, such as "Revenue, last 8 weeks: 41 to 54, low 41, high 54".

## Examples [#examples]

### In a table [#in-a-table]

Each row's trend sits in its own cell at 24px high. `dot={false}` keeps a column of sparklines quiet.

```tsx title="examples/sparkline-table.tsx"
import { Sparkline } from "@/components/dithercn/sparkline";

const PAGES = [
  {
    path: "/pricing",
    trend: [
      320, 341, 338, 362, 390, 377, 412, 431, 428, 466, 481, 503, 512, 534,
    ],
    views: "6,129",
  },
  {
    path: "/docs/installation",
    trend: [
      410, 402, 396, 421, 418, 409, 431, 426, 438, 433, 441, 437, 452, 449,
    ],
    views: "5,963",
  },
  {
    path: "/blog/dithering",
    trend: [80, 96, 141, 388, 512, 446, 371, 302, 265, 231, 214, 198, 187, 176],
    views: "4,107",
  },
  {
    path: "/changelog",
    trend: [
      262, 251, 244, 238, 229, 233, 218, 211, 206, 199, 203, 191, 187, 180,
    ],
    views: "3,052",
  },
] as const;

export default function SparklineTable() {
  return (
    <div className="bg-background border-border w-full max-w-xl overflow-hidden rounded-2xl border [--dither-paper:var(--background)]">
      <table className="w-full text-sm">
        <caption className="text-muted-foreground px-4 pt-4 pb-2 text-left text-sm">
          Top pages, last 14 days
        </caption>
        <thead>
          <tr className="text-muted-foreground border-border border-b text-xs">
            <th scope="col" className="px-4 py-2 text-left font-normal">
              Page
            </th>
            <th scope="col" className="px-4 py-2 text-left font-normal">
              Trend
            </th>
            <th scope="col" className="px-4 py-2 text-right font-normal">
              Views
            </th>
          </tr>
        </thead>
        <tbody>
          {PAGES.map((page) => (
            <tr
              key={page.path}
              className="border-border border-b last:border-0"
            >
              <td className="px-4 py-3 font-mono text-xs">{page.path}</td>
              <td className="w-28 px-4 py-2 sm:w-36">
                <Sparkline
                  data={page.trend}
                  label={`Daily views of ${page.path}`}
                  height={24}
                  dot={false}
                />
              </td>
              <td className="px-4 py-3 text-right tabular-nums">
                {page.views}
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}
```

### Line only, one in ink [#line-only-one-in-ink]

`area={false}` drops the fill. Each row shares `domain={[0, 180]}`, so a flat service draws flat instead of stretching its small wobble to full height, and the one spike drawn in ink is the first thing you see.

```tsx title="examples/sparkline-line.tsx"
import { Sparkline } from "@/components/dithercn/sparkline";

const SERVICES = [
  {
    latency: [42, 44, 41, 43, 45, 44, 42, 46, 43, 44, 42, 43],
    name: "api",
    now: "43 ms",
    spiked: false,
  },
  {
    latency: [88, 91, 87, 95, 132, 171, 158, 121, 104, 97, 93, 90],
    name: "search",
    now: "90 ms",
    spiked: true,
  },
  {
    latency: [12, 12, 13, 12, 11, 12, 13, 12, 12, 11, 12, 12],
    name: "edge-cache",
    now: "12 ms",
    spiked: false,
  },
] as const;

export default function SparklineLine() {
  return (
    <ul className="bg-background border-border flex w-full max-w-sm flex-col rounded-2xl border p-2 [--dither-paper:var(--background)]">
      {SERVICES.map((service) => (
        <li
          key={service.name}
          className="flex items-center gap-4 rounded-xl px-3 py-2.5"
        >
          <span className="w-24 font-mono text-xs">{service.name}</span>
          <Sparkline
            data={service.latency}
            label={`${service.name} p50 latency, last hour`}
            area={false}
            domain={[0, 180]}
            color={service.spiked ? "--dither-ink" : "--muted-foreground"}
            height={20}
            className="flex-1"
          />
          <span className="w-12 text-right text-sm tabular-nums">
            {service.now}
          </span>
        </li>
      ))}
    </ul>
  );
}
```

## Props [#props]

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `data` (required) | `readonly number[]` |  | Values in order, oldest first. |
| `label` (required) | `string` |  | What the trend shows. Starts the accessible summary of the data. |
| `height` | `number` | `32` | Height in px. The width follows the element. |
| `color` | `DitherColor` | `"ink"` | A palette name such as "green", or any CSS colour. |
| `area` | `boolean` | `true` | Fill under the line, densest just below it. |
| `fill` | `"gradient" \| "dotted" \| "hatched" \| "solid"` | `"gradient"` | Texture of the fill. gradient is densest just under the line. |
| `bloom` | `"off" \| "low" \| "high"` | `"off"` | A soft glow in the line's colour behind it. |
| `interactive` | `boolean` | `false` | Scrub with the pointer or arrow keys to read single values in a small label. |
| `animate` | `boolean` | `true` | Entrance motion. Always off under reduced motion. |
| `dot` | `boolean` | `true` | Mark the latest value with a dot. |
| `level` | `number` | `0.5` | Gradient only: coverage of the fill just under the line, from 0 to 1. |
| `matrix` | `"bayer2" \| "bayer4" \| "bayer8" \| "blue-noise"` | `"bayer4"` | The dither texture. |
| `cell` | `number` | `--dither-cell minus 1` | Size of one dither cell in px, rounded to whole device pixels. Smaller than other charts so the texture reads at small sizes. |
| `domain` | `[min: number, max: number]` | `The data's range` | The value range the height covers. Share one across a column so rows compare. |
| `format` | `Intl.NumberFormatOptions` | `Compact, as in 1.2K` | How values are written in the accessible summary. |

## Notes [#notes]

* By default the sparkline is a single image to assistive technology, named by `label` and a summary of its first, last, lowest and highest values, with no readout; show the exact number next to it. With `interactive` it becomes a focusable group: the arrow keys move a gliding dot and a polite live region reads each value.
* On entrance the line draws in, the fill fades up and the dot appears last. With reduced motion it appears finished.
* It runs no animation loop and costs one small SVG, so a table with dozens of rows stays cheap.