Skip to content
OpenPerf
Esc
↑↓navigate↵open⌘Jpreview
On this page

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.Tab and Metric.Panel are Tabs: the selected tab carries an accent bar and shows the panel with the same value as its name, 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 value exact and the label short. Put the full name in title.

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>
  );
}
  • delta compares with the previous period and is a Stat delta: deltaTone follows the health of the metric.
  • status takes a Badge for a verdict such as Good or Poor. Never put a verdict in delta.
  • chart takes a Sparkline with size="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 a note, 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

PropType
namestring

On Tab. Matches the `value` of its panel.

Typestring
labelReactNode

On Tab, Tile and Card.

TypeReactNode
valueReactNode

On Tab and Tile. The number shown. Optional on Card.

TypeReactNode
delta?ReactNode

On Tab, Tile and Card.

TypeReactNode
deltaTone?"neutral" | "success" | "warning" | "danger"

On Tab and Tile.

Type"neutral" | "success" | "warning" | "danger"
Default"neutral"
status?ReactNode

On Tab, Tile and Card.

TypeReactNode
chart?ReactNode

On Tab and Tile.

TypeReactNode
note?ReactNode

On Tab and Tile. A muted line under the chart.

TypeReactNode
tone?"neutral" | "success" | "warning" | "danger"

On Card.

Type"neutral" | "success" | "warning" | "danger"
Default"neutral"
unit?ReactNode

On Card. Follows the value in muted text.

TypeReactNode
aside?ReactNode

On Card.

TypeReactNode
size?"md" | "lg"

On Card.

Type"md" | "lg"
Default"md"

Parts: Root, Card, List, Panel, Tab, Tile. Root and Panel are the Tabs parts. Built on Base UI Tabs.

Was this page helpful?