Skip to content

KPI row

A dashboard's header row: key figures on one card, each with its change.

Open pipeline

$12.5M

+13.2% vs last quarter

Closed won

$3.41M

−4.2% vs last quarter

Win rate

31.8%

+3.1 pts vs last quarter

Average deal size

$66.9K

+6.5% vs last quarter

Installation

pnpm dlx shadcn@latest add @wisty/kpi-row

The CLI installs the Radix or Base UI file to match your project, with what it builds on: card, metric.

If your project doesn't have Wisty UI's theme yet, add it once with npx shadcn@latest add @wisty/cosmos. Until then, components keep shadcn's default look. Installation has the details.

When to use

Use it

  • As a dashboard's header row: the three to five numbers a page is about, the way Pipeline Inspection opens with open pipeline, closed won, win rate and average deal size.
  • When each number means most next to its change against a period, like last quarter.
  • When the row has to hold its shape while the numbers load.

Don't use it

  • For one number on its own card, or your own arrangement of label, value and trend. Use Metric.
  • For progress toward a target or quota. Use Gauge.
  • For a list of values to compare with each other. Use Bar list or Data table.

Usage

import { KpiRow } from "@/components/wisty/kpi-row"
<KpiRow
  comparison="last quarter"
  metrics={[
    {
      id: "renewals",
      label: "Renewal ARR",
      value: 2_140_000,
      previous: 1_980_000,
    },
    {
      id: "cycle",
      label: "Average sales cycle",
      value: 41,
      previous: 47,
      format: { style: "unit", unit: "day", unitDisplay: "long" },
      isIncreasePositive: false,
    },
  ]}
/>

The block formats each number and derives its change as a percent of previous: "+8.1% vs last quarter". With a percent format the change is in points. When previous is 0 it's the difference itself.

Examples

Missing figures

Open pipeline has no `previous`, so it shows no change, and quota attainment's `value` is `null`, so it says No data.

Open pipeline

$12.5M

Closed won

$3.41M

−4.2% vs last quarter

Win rate

31.8%

+3.1 pts vs last quarter

Quota attainment

No data

Loading

With `loading`, every metric holds its shape with placeholders before the numbers arrive, and the labels stay readable.

Open pipeline

Closed won

Win rate

Average deal size

API reference

KpiRow

PropTypeDefaultDescription
metrics*KpiRowMetric[]Two to five key figures, in reading order.
comparisonstring"last quarter"The comparison period, as said after "vs". Default "last quarter".
loadingbooleanfalseHolds every metric's shape with placeholders while the numbers load.
currencystring"USD"An ISO 4217 code for metrics with no format. Default "USD".
localestring"en-US"A BCP 47 locale for numbers. Default "en-US".

Also takes every <div> (opens in a new tab) prop.

KpiRowMetrictype

One key figure.

MemberTypeDefaultDescription
id*string
label*stringWhat's measured, in sentence case: "Open pipeline".
value*number | nullThe current value. null shows "No data" and no change.
previousnumber | nullThe comparison period's value. Shows the change against it.
formatIntl.NumberFormatOptionsHow the value reads, as Intl.NumberFormat options. Default an amount in the block's currency, or a plain number without one.
isIncreasePositivebooleanWhether a rise is good. Default true. Pass false for open cases, sales cycle, handle time, churn.

SLDS mapping

  • The card is slds-card, with no header: the row is the content.
  • Each metric is Metric: the label is slds-text-title, the value slds-text-heading_large, and the change takes the colors of SLDS's trend icon.
  • The placeholders are Skeleton.

Accessibility

  • The card is a group named "Key metrics". Pass aria-label to name it for the page.
  • Direction is in the signed text. Whether a change is good shows in color only, so where that isn't obvious, make the label say it.
  • While loading, each metric is busy and its value and change are placeholders. The labels stay readable.

Motion

  • The loading placeholders pulse, and stop when the user asks for reduced motion.

Responsive behavior

  • The card takes its width from its container, not its content, so give it one in a flex row.
  • Its columns follow its own width, so a row never leaves one metric alone: four go one, two by two, then four across from 44rem; five go three over two, then five across from 55rem.
  • A long label wraps. The value doesn't shrink.

Rules

  • Pass numbers to value and previous, never formatted strings, and null for a value you don't have, never 0.
  • Pass a percent as a fraction (0.318) with a percent format, so its change reads in points.
  • Set isIncreasePositive: false on every metric where a fall is good, like open cases, sales cycle, handle time or churn. Never flip the sign to get a color.
  • Write comparison as a person would say it after "vs": "last quarter", "same week last year".
  • Pass amounts in one currency and set currency to match. Mixed currencies are unavailable (value: null), not added up.
  • Never pass asChild or render.