# Inline Sparkline — React Chart Component

> A tiny table-cell sparkline that draws itself on mount, colours by trend and snaps a value bubble to the nearest point on hover.

- Category: [Charts](https://pixelpapa.com/components/charts.md)
- Tags: chart, sparkline, table, inline, svg
- Dependencies: motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/charts/chart-sparkline-inline

## Install

```bash
npx shadcn@latest add "https://pixelpapa.com/r/chart-sparkline-inline.json"
```

The file is written to `components/pixelpapa/chart-sparkline-inline.tsx`. To install manually, run `npm install motion` and copy the source below into your project. It imports `cn` from `@/lib/utils`, which every shadcn project has.

## Source

```tsx
"use client";

import { motion, useReducedMotion } from "motion/react";
import { useId, useState, type PointerEvent } from "react";
import { cn } from "@/lib/utils";

type SparklineProps = {
  values: number[];
  /** Defaults to green when the series ends higher than it starts, red otherwise. */
  color?: string;
  width?: number;
  height?: number;
  /** Soft fill under the line. */
  area?: boolean;
  /** What the numbers measure; used for the accessible name. */
  label?: string;
  formatValue?: (v: number) => string;
  className?: string;
};

export function Sparkline({
  values,
  color,
  width = 104,
  height = 30,
  area = true,
  label = "Trend",
  formatValue = (v) => v.toLocaleString("en"),
  className,
}: SparklineProps) {
  const gid = useId().replace(/[^a-zA-Z0-9]/g, "");
  const reduce = useReducedMotion();
  const [active, setActive] = useState<number | null>(null);

  const last = values.length - 1;
  const up = values[last] >= values[0];
  const stroke = color ?? (up ? "var(--chart-5)" : "var(--destructive)");
  const min = Math.min(...values);
  const span = Math.max(...values) - min || 1;
  const pad = 4;
  const pts = values.map((v, i) => ({
    x: pad + (i / Math.max(last, 1)) * (width - pad * 2),
    y: pad + (1 - (v - min) / span) * (height - pad * 2),
  }));
  const line = pts.map((p, i) => `${i ? "L" : "M"}${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(" ");
  const change = ((values[last] - values[0]) / (values[0] || 1)) * 100;
  const dot = pts[active ?? last];

  function onMove(e: PointerEvent<SVGSVGElement>) {
    const r = e.currentTarget.getBoundingClientRect();
    const x = ((e.clientX - r.left) / r.width) * width;
    setActive(Math.max(0, Math.min(last, Math.round(((x - pad) / (width - pad * 2)) * last))));
  }

  return (
    <span className={cn("relative inline-block align-middle", className)} style={{ width, height }}>
      <svg
        viewBox={`0 0 ${width} ${height}`}
        width={width}
        height={height}
        className="block touch-none overflow-visible"
        role="img"
        aria-label={`${label}: ${formatValue(values[0])} to ${formatValue(values[last])}, ${change >= 0 ? "up" : "down"} ${Math.abs(change).toFixed(0)}%`}
        onPointerMove={onMove}
        onPointerLeave={() => setActive(null)}
      >
        {area && (
          <>
            <defs>
              <linearGradient id={gid} x1="0" y1="0" x2="0" y2="1">
                <stop offset="0%" stopColor={stroke} stopOpacity={0.28} />
                <stop offset="100%" stopColor={stroke} stopOpacity={0} />
              </linearGradient>
            </defs>
            <motion.path
              d={`${line} L${pts[last].x},${height} L${pts[0].x},${height} Z`}
              fill={`url(#${gid})`}
              initial={reduce ? false : { opacity: 0 }}
              animate={{ opacity: 1 }}
              transition={{ duration: 0.6, delay: 0.5 }}
            />
          </>
        )}
        <motion.path
          d={line}
          fill="none"
          stroke={stroke}
          strokeWidth={1.6}
          strokeLinecap="round"
          strokeLinejoin="round"
          initial={reduce ? false : { pathLength: 0 }}
          animate={{ pathLength: 1 }}
          transition={{ duration: 0.9, ease: "easeOut" }}
        />
        <motion.circle
          r={active === null ? 2.2 : 3}
          fill={stroke}
          className="stroke-card"
          strokeWidth={active === null ? 0 : 1.5}
          initial={reduce ? { cx: dot.x, cy: dot.y } : { cx: dot.x, cy: dot.y, opacity: 0 }}
          animate={{ cx: dot.x, cy: dot.y, opacity: 1 }}
          transition={{ cx: { duration: 0.12 }, cy: { duration: 0.12 }, opacity: { delay: 0.8 } }}
        />
      </svg>
      {active !== null && (
        <span
          className="pointer-events-none absolute z-10 -translate-x-1/2 -translate-y-full rounded-md bg-foreground px-1.5 py-0.5 text-[10px] font-medium whitespace-nowrap text-background tabular-nums shadow-md"
          style={{ left: dot.x, top: dot.y - 6 }}
        >
          {formatValue(values[active])}
        </span>
      )}
    </span>
  );
}

const pages = [
  { path: "/pricing", views: 48210, series: [31, 34, 33, 38, 41, 40, 46, 52, 49, 57, 61, 66] },
  { path: "/docs/quickstart", views: 36940, series: [44, 43, 47, 45, 49, 52, 50, 55, 54, 58, 57, 60] },
  { path: "/blog/motion-budget", views: 21875, series: [8, 9, 14, 31, 58, 49, 37, 30, 26, 22, 21, 19] },
  { path: "/changelog", views: 12430, series: [27, 26, 24, 25, 22, 23, 20, 19, 21, 18, 17, 15] },
  { path: "/templates", views: 9615, series: [12, 13, 12, 15, 14, 17, 19, 18, 22, 24, 23, 27] },
];

export default function ChartSparklineInlineDemo() {
  return (
    <div className="w-full max-w-xl overflow-hidden rounded-2xl border border-border bg-card shadow-sm">
      <table className="w-full text-sm">
        <thead>
          <tr className="border-b border-border text-left text-xs text-muted-foreground">
            <th className="px-5 py-3 font-medium">Page</th>
            <th className="px-3 py-3 text-right font-medium">Views</th>
            <th className="px-3 py-3 text-right font-medium">12 wk</th>
            <th className="py-3 pr-5 pl-3 text-right font-medium">Trend</th>
          </tr>
        </thead>
        <tbody>
          {pages.map((p) => {
            const change = ((p.series[p.series.length - 1] - p.series[0]) / p.series[0]) * 100;
            return (
              <tr key={p.path} className="border-b border-border transition-colors last:border-0 hover:bg-muted/50">
                <td className="px-5 py-3 font-mono text-[13px] text-card-foreground">{p.path}</td>
                <td className="px-3 py-3 text-right text-card-foreground tabular-nums">{p.views.toLocaleString("en")}</td>
                <td
                  className={cn(
                    "px-3 py-3 text-right font-medium tabular-nums",
                    change >= 0 ? "text-emerald-600 dark:text-emerald-400" : "text-destructive",
                  )}
                >
                  {change >= 0 ? "+" : "−"}
                  {Math.abs(change).toFixed(0)}%
                </td>
                <td className="py-2 pr-5 pl-3 text-right">
                  <Sparkline values={p.series} label={`${p.path} weekly views`} formatValue={(v) => `${v}k`} />
                </td>
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
}
```

## AI prompt

```text
Add the "Inline Sparkline" component from PixelPapa UI to this project.

What it is: A tiny table-cell sparkline that draws itself on mount, colours by trend and snaps a value bubble to the nearest point on hover.
Category: Charts. Depends on: motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/chart-sparkline-inline.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/chart-sparkline-inline.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/chart-sparkline-inline" and place it where I describe below.
3. Keep its animation timings and accessibility attributes. Replace the demo copy and data with content that fits my product, and use my existing theme tokens (bg-background, text-foreground, bg-primary, border-border) rather than hard-coded colours.

Where to use it: <describe the page and position here>
```

## More charts

- [Area Chart Gradient](https://pixelpapa.com/components/charts/chart-area-gradient.md) (Pro): A smooth SVG area chart that draws its line on mount, fades in a gradient fill and snaps a crosshair tooltip to the nearest point.
- [Donut Chart Segments](https://pixelpapa.com/components/charts/chart-donut-segments.md): A donut whose segments sweep in one after another; hovering a segment or its legend row thickens it and swaps the centre total for that slice.
- [Funnel Steps Chart](https://pixelpapa.com/components/charts/chart-funnel-steps.md) (Pro): A flowing horizontal funnel: each stage tapers into the next with a bezier, stages fade in left to right and hovering one shows its drop-off.
- [Grouped Bar Chart](https://pixelpapa.com/components/charts/chart-bar-grouped.md) (Pro): Side-by-side SVG bars per category that grow from the baseline in a stagger, with a group highlight and a tooltip comparing every series.
- [Heat Grid Week by Hour](https://pixelpapa.com/components/charts/chart-heat-grid.md) (Pro): A 7×24 SVG heatmap whose cells ripple in diagonally; hover or arrow-key between cells for a snapped tooltip, with peak hour called out.
- [Multi Line Chart With Legend](https://pixelpapa.com/components/charts/chart-line-multi-legend.md) (Pro): Several SVG lines draw in together; legend buttons toggle a series and the y-axis rescales while the remaining lines morph to fit.
