Metric
The headline numbers of a page, as tabs that pick a chart, as tiles or as cards.
import { Metric } from "@openperf/deft";
import { Preview } from "../../src/Preview";
const METRICS = [
{ name: "metric-1", label: "Metric 1", value: 12 },
{ name: "metric-2", label: "Metric 2", value: 48 },
{ name: "metric-3", label: "Metric 3", value: 120 },
];
export default function MetricBasic() {
return (
<Preview block={true}>
<Metric.Root defaultValue="metric-1">
<Metric.List aria-label="Metrics">
{METRICS.map((metric) => (
<Metric.Tab
key={metric.name}
label={metric.label}
name={metric.name}
value={metric.value}
/>
))}
</Metric.List>
{METRICS.map((metric) => (
<Metric.Panel key={metric.name} value={metric.name}>
{metric.label}
</Metric.Panel>
))}
</Metric.Root>
</Preview>
);
}
- Use metric tabs for the three to six headline numbers of an overview page. For numbers that don’t switch a chart, use a Stat.
Metric.Root,Metric.List,Metric.TabandMetric.Panelare Tabs: the selected tab carries an accent bar and shows the panel with the samevalueas itsname, and arrow keys move between tabs.- Give every tab a panel; a number without a chart to switch to does not belong in the row.
- The list is a grid of equal columns that scrolls sideways when it runs out of room. It bleeds into the gutter so the text lines up with the page edge; don’t add margins around it.
- Keep the
valueexact and thelabelshort. Put the full name intitle.
Tab content
import { Badge, Metric, Sparkline } from "@openperf/deft";
import { Preview } from "../../src/Preview";
export default function MetricTabContent() {
return (
<Preview block={true}>
<Metric.Root defaultValue="metric-1">
<Metric.List aria-label="Metrics">
<Metric.Tab
chart={<Sparkline.Root size="fill" values={[12, 24, 18, 36, 30, 48]} />}
delta="+12%"
label="Metric 1"
name="metric-1"
note="Note"
status={<Badge.Root>Status</Badge.Root>}
value={48}
/>
<Metric.Tab
chart={<Sparkline.Root size="fill" values={[48, 36, 60, 72, 96, 120]} />}
delta="+48%"
deltaTone="warning"
label="Metric 2"
name="metric-2"
note="Note"
status={<Badge.Root tone="warning">Status</Badge.Root>}
value={120}
/>
</Metric.List>
<Metric.Panel value="metric-1">Metric 1</Metric.Panel>
<Metric.Panel value="metric-2">Metric 2</Metric.Panel>
</Metric.Root>
</Preview>
);
}
deltacompares with the previous period and is a Stat delta:deltaTonefollows the health of the metric.statustakes a Badge for a verdict such as Good or Poor. Never put a verdict indelta.charttakes aSparklinewithsize="fill"or a meter. The chart slot keeps its height when empty, so tabs in a row line up. Give every tab in a row anote, or none.
Tiles
import { Metric, Sparkline } from "@openperf/deft";
import { Preview } from "../../src/Preview";
const METRICS = [
{ label: "Metric 1", value: 12, delta: "+12%", values: [12, 24, 18, 36, 30, 48] },
{ label: "Metric 2", value: 48, delta: "+48%", values: [48, 36, 60, 72, 96, 120] },
{ label: "Metric 3", value: 120, delta: "−12%", values: [120, 96, 72, 60, 48, 36] },
];
export default function MetricTiles() {
return (
<Preview block={true}>
<div className="grid grid-cols-3 gap-8">
{METRICS.map((metric) => (
<Metric.Tile
chart={<Sparkline.Root size="fill" values={metric.values} />}
delta={metric.delta}
key={metric.label}
label={metric.label}
value={metric.value}
/>
))}
</div>
</Preview>
);
}
Metric.Tile is a tab’s content without the tab: the same label, value, delta and chart for numbers that switch nothing, such as the summary above a list. Lay tiles out in a grid or a wrapping row.
Cards
import { Dot, Meter, Metric } from "@openperf/deft";
import { Preview } from "../../src/Preview";
const CARDS = [
{ label: "Metric 1", tone: "neutral", dot: "info", status: "Normal", value: 120, previous: 120 },
{
label: "Metric 2",
tone: "warning",
dot: "warning",
status: "Degraded",
value: 120,
previous: 96,
},
{ label: "Metric 3", tone: "danger", dot: "error", status: "Critical", value: 120, previous: 48 },
] as const;
const COMPARE_TONES = { neutral: "story", warning: "warning", danger: "danger" } as const;
export default function MetricCards() {
return (
<Preview block={true}>
<div className="flex flex-col gap-3">
{CARDS.map((card) => (
<Metric.Card
aside={
<Meter.Compare
current={{ label: "Current", text: `${card.value} ms`, value: card.value }}
previous={{ label: "Previous", text: `${card.previous} ms`, value: card.previous }}
tone={COMPARE_TONES[card.tone]}
/>
}
delta={card.value === card.previous ? "≈ no change" : "▲ 12%"}
key={card.label}
label={card.label}
status={<Dot.Root tone={card.dot}>{card.status}</Dot.Root>}
tone={card.tone}
unit="ms"
value={card.value}
/>
))}
</div>
</Preview>
);
}
Metric.Card is the large form for the key metrics of an overview. Its tone follows the health of the metric and tints the card and the delta; put the health label, a Dot, in status. aside sits beside the value, which keeps a minimum width so the asides of stacked cards line up, and takes a Meter.Compare with the previous period; children go under it and take the chart. Use size="lg" for the one card that holds a chart. A card without value shows only its label and children, for a loading state.
API
namestring
On Tab. Matches the `value` of its panel.
stringlabelReactNode
On Tab, Tile and Card.
ReactNodevalueReactNode
On Tab and Tile. The number shown. Optional on Card.
ReactNodedelta?ReactNode
On Tab, Tile and Card.
ReactNodedeltaTone?"neutral" | "success" | "warning" | "danger"
On Tab and Tile.
"neutral" | "success" | "warning" | "danger""neutral"status?ReactNode
On Tab, Tile and Card.
ReactNodechart?ReactNode
On Tab and Tile.
ReactNodenote?ReactNode
On Tab and Tile. A muted line under the chart.
ReactNodetone?"neutral" | "success" | "warning" | "danger"
On Card.
"neutral" | "success" | "warning" | "danger""neutral"unit?ReactNode
On Card. Follows the value in muted text.
ReactNodeaside?ReactNode
On Card.
ReactNodesize?"md" | "lg"
On Card.
"md" | "lg""md"Parts: Root, Card, List, Panel, Tab, Tile. Root and Panel are the Tabs parts. Built on Base UI Tabs.