Design Lounge
Install

Nº 119 of 520

KPI row with sparklines

Four Harbor Ops KPI cards: label, large number, signed delta, and an SVG sparkline that draws on load and highlights the nearest point on hover.

Preview

Open full size
1280 × 800 · 6.6 KB · 109 lines

Use it

Free to use · credit appreciated

1 Once, in your project

npx skills add SusanAcharya/Design-Lounge

2 Then ask

Using the Design Lounge skill, build "KPI row with sparklines" (piece charts-kpi-spark-row) in my stack. Match its brief; restyle it onto my product's theme.

No skill? Paste the brief instead. It carries the numbers, tokens and behaviour.

Brief

179 lines

Build brief for a coding agent. Rebuild this piece in the reader’s stack. If they haven’t said which stack, ask once, then default to semantic HTML + CSS + a little vanilla JS. Match the numbers below; don’t “improve” them.

What it is

A single dashboard row for Harbor Ops, a fictional container-terminal tool. Four cards in a 12px gap: Throughput, Berth util., Avg. delay, Fuel / move. Each card is a 11px mono label, a 40px tabular number with a unit, a signed delta versus the prior shift, and a 56px-tall SVG sparkline of the last 12 hours. On load each polyline draws itself with pathLength="1" and a dashoffset animation. Hovering the spark snaps a 3.5px cobalt dot and a mono tooltip to the nearest of the 12 points. One cobalt, cool gray paper, 2px radii. Replay rebuilds the cards so the lines draw again.

Reference behaviour

  1. Initial state: cool gray page #e6e8ec. Header 56px: 10px cobalt square + “Harbor Ops” 14/600, then mono “Berth 12 · Shift 06:00–14:00 · 2 Oct 2026”, then a Replay button.
  2. Kicker under the header: “Live KPIs · last 12 hours”, 11px mono uppercase --ink-3.
  3. Four cards, repeat(4, 1fr), gap 12, min-height 220, padding 22/18, background #f4f5f7, 1px #cfd3da border, radius 2.
  4. Card contents, top to bottom: label, number, delta, spark (margin-top auto).
  5. Deltas: Throughput +6.2% green + chevron-up; Berth util. +1.1 pt green + up; Avg. delay −4 min green + chevron-down (delay fell, which is good); Fuel / move −0.18 t green + down. Colour is --up (#2a7a5a) for all four because every change is an improvement. Arrow direction follows the sign of the number.
  6. On load each spark path uses pathLength="1", stroke-dasharray: 1, stroke-dashoffset: 1, then animates dashoffset to 0 over 900ms expo-out. Stroke cobalt, 1.75px, round caps.
  7. Pointer-move on the SVG: convert clientX into viewBox X (0–160), pick the closest of 12 points, show .hi circle and .tip with the formatted value (148 TEU, 86.4%, 12 min, 2.41 t). Pointer-leave hides the highlight.
  8. Replay empties #row and rebuilds the four cards so the draw animation runs again.
  9. Reduced motion: no draw animation; paths render complete (stroke-dashoffset: 0). Hover highlight still works.

Structure

1280 × 800
┌────────────────────────────────────────────────────────────┐
│ ■ Harbor Ops      Berth 12 · Shift 06:00–14:00 · 2 Oct  [Replay] │ 56
├────────────────────────────────────────────────────────────┤
│ LIVE KPIS · LAST 12 HOURS                                  │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐            │
│ │THROUGH. │ │BERTH    │ │AVG DELAY│ │FUEL/MOVE│            │
│ │ 148 TEU │ │ 86.4 %  │ │ 12 min  │ │ 2.41 t  │            │ 40px nums
│ │ +6.2%   │ │ +1.1 pt │ │ −4 min  │ │ −0.18 t │            │
│ │ ╱‾╲╱‾   │ │ ╱‾‾‾    │ │ ╲___    │ │ ╲___    │            │ spark 48
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘            │
│         cards centered in the remaining height             │
└────────────────────────────────────────────────────────────┘
  page padding 40
  • <header> flex row. Brand, .shift, #replay.
  • <main> flex column, justify-content: center, padding 0 40px 48px.
  • #row grid of four <article class="card"> built in JS from a KPIS array. Each card: .lbl, .num + <small> unit, .delta, .spark (SVG + <b class="tip">).
  • Spark SVG viewBox="0 0 160 56", preserveAspectRatio="none", role="img" and aria-label="{label} trend". Path pathLength="1", circle .hi r=3.5.

KPI data (last 12 hours, oldest → newest)

LabelNumberUnitDeltaDirSeries
Throughput148TEU+6.2%up92, 98, 104, 101, 110, 118, 122, 128, 124, 136, 142, 148
Berth util.86.4%+1.1 ptup78, 80, 79, 81, 82, 83, 84, 83, 85, 86, 85, 86.4
Avg. delay12min−4 mindown22, 20, 19, 18, 17, 16, 15, 16, 14, 13, 13, 12
Fuel / move2.41t−0.18 tdown2.8, 2.74, 2.7, 2.66, 2.62, 2.58, 2.55, 2.52, 2.5, 2.47, 2.44, 2.41

Tooltip formatters: v + ' TEU', v.toFixed(1) + '%', v + ' min', v.toFixed(2) + ' t'.

Tokens

:root {
  --bg: #e6e8ec;               /* page */
  --card: #f4f5f7;             /* card surface */
  --ink: #16191e;
  --ink-2: #5a616b;
  --ink-3: #8b929c;
  --line: #cfd3da;
  --cobalt: #1f4ebf;           /* the one accent, spark stroke, mark */
  --up: #2a7a5a;               /* improving delta */
  --down: #b5473a;             /* reserved; unused in this data set */
  --sans: "IBM Plex Sans", system-ui, sans-serif;
  --mono: "IBM Plex Mono", ui-monospace, monospace;
  --gutter: 40px;
  --r: 2px;
  --t-micro: 160ms;
  --t-draw: 900ms;
  --ease: cubic-bezier(.2, .7, .2, 1);
  --expo: cubic-bezier(.16, 1, .3, 1);
}

Typography

RoleFamilySizeWeightLine-heightTrackingCase
NumberIBM Plex Sans40px6001−0.04emtabular
UnitIBM Plex Sans16px50010as-is
BrandIBM Plex Sans14px6001−0.01emTitle
LabelIBM Plex Mono11px5001+0.1emUPPERCASE
DeltaIBM Plex Mono12px50010as-is
Shift / tipIBM Plex Mono12/11px400/50010as-is
KickerIBM Plex Mono11px5001+0.12emUPPERCASE

Numbers use font-variant-numeric: tabular-nums.

Motion

ElementTriggerPropertyFrom → ToDurationEasing
Spark pathload / replaystroke-dashoffset1 → 0900ms--expo
Tip / dotpointer enteropacity0 → 1160ms--ease
Replay hoverhovercolor, borderink-3 → ink160ms--ease

pathLength="1" makes dasharray 1 equal the whole polyline, independent of pixel length. Reduced motion: animation: none; stroke-dashoffset: 0.

States

  • Spark hover: .spark.on reveals .hi and .tip. Leaving restores the last point invisibly (dot hidden).
  • Delta: class up → --up. Class dn is in the CSS for regressions but this data set does not use it.
  • Replay hover: border and type --ink.
  • Focus-visible: 2px cobalt outline, 3px offset, on Replay.

Accessibility

  • Each spark SVG has role="img" and aria-label like “Throughput trend”. The 12 sample values are in the label’s number and delta; the hover tooltip is mouse-only extra detail, not the only place the figure appears.
  • Cards are <article>. The row is not a data table; four independent metrics.
  • Replay is a <button>.
  • Contrast: ink on card ~14:1. --ink-2 on card ~6:1. Cobalt on card ~5.5:1 for the 1.75px stroke (decorative). --ink-3 is 11px uppercase labels.
  • Tooltip is <b class="tip">, pointer-events: none, not a live region (it would chatter on every mouse move).

Responsive rules

  • ≥ 1280: four columns, numbers 40px.
  • 1024–1279: four columns, numbers 32px, padding 16.
  • 768–1023: two columns, two rows. Sparks stay 48px tall.
  • < 640: one column. Header stacks brand and shift.

Acceptance checklist

  • Exactly four cards in one row at 1280, gap 12, radius 2, cool gray + one cobalt.
  • Numbers are 40px/600 IBM Plex Sans, tabular, with a 16px unit.
  • Sparklines are inline SVG, 12 points, cobalt 1.75px, viewBox 160×56, and draw 1 → 0 dashoffset over 900ms on load.
  • Hovering a spark moves a 3.5px dot and a mono tooltip to the nearest point; leaving hides them.
  • Delay and fuel use a down chevron and green type (improvement). Throughput and util use an up chevron.
  • Replay rebuilds the row and the lines draw again.
  • Reduced motion shows complete lines with no dash animation; hover still works.
  • Shift line reads Berth 12, 06:00–14:00, 2 Oct 2026.
  • Only IBM Plex Sans and IBM Plex Mono load.
  • No chart library, no canvas, no purple.

Implementation notes

Normalise the path to length 1 so every spark draws in the same 900ms:

<path d="M4,30 L…" pathLength="1" />
.spark path {
  fill: none;
  stroke: var(--cobalt);
  stroke-width: 1.75;
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  animation: draw 900ms cubic-bezier(.16, 1, .3, 1) forwards;
}
@keyframes draw { to { stroke-dashoffset: 0; } }

Map pointer X to the nearest point, not to a linear interpolation, so the tooltip always matches a real sample:

svg.addEventListener('pointermove', (e) => {
  const r = svg.getBoundingClientRect();
  const x = (e.clientX - r.left) / r.width * 160;
  const best = pts.reduce((a, p) => Math.abs(p.x - x) < Math.abs(a.x - x) ? p : a);
  set(best);
});

Y mapping: y = 56 - 4 - (v - min) / (max - min) * 48. If max === min, use a span of 1 so the line sits in the middle instead of dividing by zero.

Common mistakes: animating stroke-dasharray with a guessed pixel length (the four series have different path lengths, so they finish at different times). Using a filled area under the line. Colouring a falling delay red.

Details

Palette
Type
IBM Plex Sans · IBM Plex Mono
Motion
Subtle motion
Build
One session
Designed bySusan AcharyaKathmandu

Studied

Don't Board Me

A control. It studies this site for quiet, not for a show.

All sources