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
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 onMetricValueand 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
└── MetricTrendimport {
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.
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
| Part | Built 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
| Prop | Type | Default | Description |
|---|---|---|---|
loading | boolean | false | Whether the number is still loading: the parts draw placeholders and the metric is aria-busy. |
MetricTrend
| Prop | Type | Default | Description |
|---|---|---|---|
change* | number | The signed change; its sign sets the arrow, flat at 0. | |
isIncreasePositive | boolean | true | Whether 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.
| Parameter | Type | Default |
|---|---|---|
value* | number | |
__1 | MetricFormatOptions | {} |
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.
| Parameter | Type | Default |
|---|---|---|
value* | number | |
previous* | number | |
__2 | MetricFormatOptions | {} |
Returns string.
MetricFiguretype
One key figure and the comparison period's value, as a block takes it.
| Member | Type | Default | Description |
|---|---|---|---|
id* | string | ||
label* | string | What's measured, in sentence case: "Open pipeline". | |
value* | number | null | The current value. null shows "No data" and no change. | |
previous | number | null | The comparison period's value. Shows the change against it. | |
format | Intl.NumberFormatOptions | How the value reads, as Intl.NumberFormat options. Default an amount in the block's currency, or a plain number without one. | |
isIncreasePositive | boolean | Whether a rise is good. Default true. Pass false for open cases, sales cycle, handle time, churn. |
MetricFormatOptionstype
| Member | Type | Default | Description |
|---|---|---|---|
format | Intl.NumberFormatOptions | Intl.NumberFormat options. A notation or digits here win over the defaults. | |
locale | string | A 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.
| Prop | Value | SLDS | Notes |
|---|---|---|---|
MetricLabel | (part) (added) | slds-text-title | Names the metric. A plain p, read just before the value. |
MetricValue | (part) (added) | slds-text-heading_large | 32px in Cosmos, 28px in Lightning Blue, with tabular figures. |
MetricUnit | (part) (added) | none | A smaller suffix after the value. |
MetricTrend change | number (signed) (added) | slds-icon-trend data-slds-trend | The 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 isIncreasePositive | true (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 loading | true, 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.
Metricadds no role. A metric with noMetricLabelneeds a name right before it, like a card title, orrole="group"witharia-labelledbypointing 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 chartaccessibilityLayer={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, addisIncreasePositive={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
loadingonMetricand 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
asChildorrender.