Skip to content

Metric

Shows one number with its name, a unit and a trend colored good or bad.

Closed won this quarter

$1.2M

+16% vs last quarter

Installation

pnpm dlx shadcn@latest add @wisty/metric

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

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

  • A dashboard or record page shows one number that matters on its own, like sales this quarter or open cases.
  • People need to see whether the number went up or down, and whether that's good.
  • A row of key figures sits above the charts that explain them.

Don't use it

  • For progress toward a target, or a value with a maximum. Use Gauge, which draws the value against the target and colors it by bands. A plain value colored by the range it falls in, as Lightning's dashboard Metric does with segment ranges, is the Segment ranges example: the range's color on MetricValue and its name under the value.
  • For values across categories or over time. Use Chart. One value never needs a chart.
  • For a status. Use Badge.
  • For a list of labels and values on a record. Use the record's fields.

Usage

Metric
├── MetricLabel
├── MetricValue
│   └── MetricUnit
└── MetricTrend
import {
  Metric,
  MetricLabel,
  MetricTrend,
  MetricUnit,
  MetricValue,
} from "@/components/wisty/metric"

const signed = new Intl.NumberFormat("en-US", { signDisplay: "exceptZero" })
const change = 4 // from your data
<Metric>
  <MetricLabel>Win rate</MetricLabel>
  <MetricValue>
    32
    <MetricUnit>%</MetricUnit>
  </MetricValue>
  <MetricTrend change={change}>
    {signed.format(change)} pts vs last quarter
  </MetricTrend>
</Metric>

The trend's text and its change come from the same number, so the arrow and the words never disagree. Format the value before you pass it, with a fixed locale, so server and client print the same text. A word unit takes a space before it, as it's written ({days} <MetricUnit>days</MetricUnit>); a percent sign takes none. formatMetricValue formats a number as KPI row and Metric trend do: written out under 10,000, compact from there to three significant digits ("$4.82M"). formatMetricChange writes the change from a previous value as signed text: a percent of it, points for a percent format, and the difference itself when the previous value is 0. A sparkline, a card title in place of the label, and the loading state are shown by the In a card, with a sparkline and Loading examples.

Examples

States

Rises and falls colored by `isIncreasePositive`, with worse or better in the text, no change, no trend, and three values with a `MetricUnit`.

Win rate

32%

+4 pts vs last quarter

Open cases

128

+12% vs last week, worse

Churn

2.1%

-0.4 pts vs last quarter, better

Average deal age

41 days

No change vs last quarter

Active accounts

1,480

In a card, with a sparkline

Open pipeline in a card whose title names it in place of `MetricLabel`, with eight weeks as a decorative sparkline under the number.

Open pipeline

$3.9M

+25% over 8 weeks

Segment ranges

Three values colored low, middle or high by their own breakpoints, with the range named under each value, so color isn't the only cue.

Win rate

18%

Below target

Pipeline coverage

2.6x

Near target

Quota attainment

104%

On target

Loading

Two metrics with `loading` on `Metric`, beside one that has loaded: the labels stay, and the value and trend are placeholders of their own size.

Closed won this quarter

Win rate

Open pipeline

$4.8M

+14.5% vs last quarter

API reference

PartBuilt on
Metric<div> (opens in a new tab)
MetricLabel<p> (opens in a new tab)
MetricValue<p> (opens in a new tab)
MetricUnit<span> (opens in a new tab)
MetricTrend<p> (opens in a new tab)

Metric

PropTypeDefaultDescription
loadingbooleanfalseWhether the number is still loading: the parts draw placeholders and the metric is aria-busy.

MetricTrend

PropTypeDefaultDescription
change*numberThe signed change; its sign sets the arrow, flat at 0.
isIncreasePositivebooleantrueWhether a rise is good; sets the color. Pass false for churn or open cases.

formatMetricValuefunction

A dashboard number: written out under 10,000 with at most one decimal, compact from 10,000 to three significant digits ($4.82M, 12.9K), so neighbors compare at the same precision.

ParameterTypeDefault
value*number
__1MetricFormatOptions{}

Returns string.

formatMetricChangefunction

The change from previous to value as signed text: a percent of the previous value, points for a percent format, the difference itself when the previous value is 0, and "No change" at none. Minus is a true minus.

ParameterTypeDefault
value*number
previous*number
__2MetricFormatOptions{}

Returns string.

MetricFiguretype

One key figure and the comparison period's value, as a block takes it.

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.

MetricFormatOptionstype

MemberTypeDefaultDescription
formatIntl.NumberFormatOptionsIntl.NumberFormat options. A notation or digits here win over the defaults.
localestringA BCP 47 locale. Default "en-US".

SLDS mapping

SLDS has no metric component. Its Charts guidelines name Metric as an intent, and Lightning's dashboard Metric shows one value. The parts follow SLDS's type classes and the meaning of its trend icon, slds-icon-trend; the table lists the class behind each part. The trend's arrow is Lucide's, turned as SLDS turns its own. While loading, the value and the trend are Skeleton.

Known gaps:

  • SLDS's trend icon draws a circle around the arrow and can animate it. The metric draws the arrow only, and never animates it.
PropValueSLDSNotes
MetricLabel(part) (added)slds-text-titleNames the metric. A plain p, read just before the value.
MetricValue(part) (added)slds-text-heading_large32px in Cosmos, 28px in Lightning Blue, with tabular figures.
MetricUnit(part) (added)noneA smaller suffix after the value.
MetricTrend changenumber (signed) (added)slds-icon-trend data-slds-trendThe arrow comes from the sign: up above 0, down below 0, flat at 0. Lucide's arrow, turned as SLDS turns its trend arrow.
MetricTrend isIncreasePositivetrue (default), false (added)slds-icon-trend colors (success-1, error-1, on-surface-1)The color. By default a rise is positive and a fall negative; false flips them, as for churn. No change is always neutral.
Metric loadingtrue, false (default) (added)none (aria-busy; the free Skeleton)While true, MetricValue and MetricTrend draw placeholders of their own size instead of their children, and the metric is aria-busy. The label stays.

Accessibility

  • A metric is plain text in reading order: label, value, trend. Metric adds no role. A metric with no MetricLabel needs a name right before it, like a card title, or role="group" with aria-labelledby pointing at one.
  • The arrow is hidden from screen readers, so the trend's text must carry the sign and the comparison ("+16% vs last quarter"). Good or bad is in the color only; when it matters, write it too ("+12% vs last week, worse").
  • A sparkline is decoration. Wrap it in aria-hidden="true" and give the chart accessibilityLayer={false}, so it takes no tab stop, and say what it shows in the trend's text.
  • A loading metric is aria-busy, with no text in its value and trend, so a screen reader never hears a stale or zero figure.

Motion

  • The value doesn't animate, so a changed figure jumps into place. The loading placeholders pulse, and hold still when the user prefers reduced motion.

Responsive behavior

  • A metric fills the width of its parent. Put metrics in a grid or a wrapping row with a gap.
  • A long label wraps. The value doesn't shrink, so keep values compact ("$1.2M", not "$1,240,000").

Performance

  • A sparkline is a full chart. Keep to a few per page, with a few dozen points each.

Rules

  • Pass the signed change from the data as change, never a literal. When a rise is bad, add isIncreasePositive={false}; never flip the sign to get a color.
  • Give a sparkline isAnimationActive={false} and <YAxis hide domain={["dataMin", "dataMax"]} />. Without the axis the line sits flat near the top instead of showing its shape.
  • While the number loads, pass loading on Metric and render the same parts as when loaded, so the placeholders match what replaces them. Never show a zero, a dash or a stale figure in their place.
  • A currency symbol belongs in the formatted value, not in MetricUnit.
  • Never pass asChild or render.