HomeJournalThis post

Build a Stripe-style sparkline in 80 lines of SVG

From normalization math to area gradients and smooth metric morphs.

JP
JP Casabianca
Designer/Engineer · Bogotá

A good sparkline is one of the highest-leverage UI details in a dashboard. It gives shape to a metric without asking the reader to parse a full chart. It can show momentum, volatility, seasonality, or recovery in the corner of a card.

The mistake is treating sparklines like miniature analytics products. A sparkline should not compete with the page. It should support a decision in one glance.

This tutorial builds a Stripe-style sparkline with plain SVG: a smooth line, a soft area fill, one annotation point, and a hover label. No charting library is required. The point is not to avoid libraries forever. The point is to understand the geometry well enough to make better choices when you do use one.

Peak +14%
Figure 1: A sparkline usually needs three things: normalized points, a readable path, and one useful annotation. Everything else is optional.

1. Normalize the values

SVG coordinates start at the top-left, which means higher data values need lower y positions. Normalization maps raw values into the drawing box.

function normalize(values, width, height, pad = 10) {
  const min = Math.min(...values);
  const max = Math.max(...values);
  const range = max - min || 1;

  return values.map((value, index) => {
    const x = pad + (index * (width - pad * 2)) / (values.length - 1);
    const y = height - pad - ((value - min) / range) * (height - pad * 2);
    return { x, y, value };
  });
}

This function does two important things. First, it spreads points evenly across the available width. Second, it protects against the flat-line case where every value is the same. Without the range || 1 guard, a quiet metric can turn into NaN coordinates.

2. Start with straight lines

Before smoothing anything, draw the simplest possible path. This is the version that is easiest to debug.

function linePath(points) {
  return points
    .map((point, index) => (index === 0 ? "M" : "L") + point.x + "," + point.y)
    .join(" ");
}

If the chart looks wrong at this stage, smoothing will only hide the error. Check the first and last points. Check whether the highest value sits near the top of the viewBox. Check whether the lowest value sits near the bottom.

3. Smooth with intent

Stripe-style sparklines often look smooth, but "smooth" is not the same thing as "accurate." Too much curve can imply trends that do not exist. For financial, conversion, or operational data, the safest default is a modest curve.

A simple middle ground is to use a cubic curve between points with control points based on neighboring x positions. You do not need a full spline library for a small metric card.

function smoothPath(points) {
  return points.reduce((path, point, index) => {
    if (index === 0) return "M" + point.x + "," + point.y;

    const previous = points[index - 1];
    const controlX = (previous.x + point.x) / 2;
    return path + " C" + controlX + "," + previous.y + " " + controlX + "," + point.y + " " + point.x + "," + point.y;
  }, "");
}

This gives the line enough polish without turning it into decoration. The data still controls the shape.

4. Add the area fill

The area fill is the line path closed down to the baseline. It adds depth and makes the metric card feel intentional.

const areaPath = path + " L" + last.x + "," + (height - pad) + " L" + first.x + "," + (height - pad) + " Z";

<path d={areaPath} fill="url(#area)" />
<path d={path} fill="none" stroke="currentColor" strokeWidth="2.5" />

The gradient should fade quickly. If the fill is too strong, the chart starts competing with labels and numbers. A sparkline should be noticeable only after the primary metric has done its job.

5. Annotate one useful moment

Most metric cards do not need full tooltips. They need one clue: the latest value, the peak, the dip, or the point where a rollout happened.

Choose the annotation based on the question the card answers. If the card is about growth, annotate the most recent value. If it is about reliability, annotate an incident dip. If it is about an experiment, annotate the launch date.

const peak = points.reduce((best, point) =>
  point.value > best.value ? point : best
);

Then render a small dot and label. Keep the label close to the point, but not on top of the line.

6. Make hover a progressive enhancement

Hover is useful on desktop and fragile on mobile. The sparkline should still make sense without interaction. If you add hover, treat it as an extra layer.

<svg onMouseMove={handleMove} onMouseLeave={() => setActive(null)}>
  <path d={areaPath} fill="url(#area)" />
  <path d={path} />
  {active && <circle cx={active.x} cy={active.y} r="4" />}
</svg>

For mobile, consider showing the latest value permanently and skipping hover entirely. Touch charts often become awkward when the surrounding card is also clickable.

7. Decide when not to build this

Use this hand-built approach for small, decorative, or tightly controlled metric cards. Reach for a charting library when you need axes, legends, brushing, zooming, accessibility tables, multiple series, or user-configurable data.

The goal is not to become a charting library. The goal is to understand enough SVG to make a lightweight component that behaves exactly as the product needs.

Use SVGSmall cards

One series, one story, no heavy interaction.

Use a libAnalysis tools

Axes, legends, filters, multiple series, exports.

Use textNo trend

If the shape does not change the decision, do not draw it.

Figure 2: The right charting approach depends on the decision surface, not on the engineer's appetite for custom SVG.

Production details that matter

Once the line works, the remaining work is product polish. Give the SVG a stable aspect ratio so the card does not jump during layout. Use vector-effect="non-scaling-stroke" if the sparkline appears in flexible containers and the stroke starts looking too thick or too thin. Add an accessible label that summarizes the trend, because the shape alone is not useful to screen-reader users.

Also decide how the chart behaves when data is incomplete. Missing values should not quietly become zeros unless zero is a real measurement. For operational dashboards, I prefer to break the line or show a muted unavailable state. A fake dip can send someone looking for a problem that does not exist.

The last detail is animation. A subtle transition between paths can make metric changes feel premium, but it should never obscure the value. Keep duration short, respect reduced-motion preferences, and avoid animating every card at once on page load. When charts animate too eagerly, the dashboard starts feeling like a demo instead of a tool.

A review checklist

Before shipping a sparkline component, check it against the job it is supposed to do:

  • Can someone understand the metric before noticing the chart?
  • Does the line still read at the smallest card size?
  • Does the annotation answer a useful question?
  • Are empty, flat, and single-point datasets handled?
  • Is the hover state optional rather than required?
  • Does the chart have a text alternative?
  • Would a full chart be more honest for this data?

That last question is the most important. A sparkline is not a replacement for analysis. It is a small signal inside a larger interface.

A sparkline is successful when nobody admires it for long. They see the metric, understand the shape, and move on with more confidence.