---
title: Metric
description: The headline numbers of a page, as cards or as card tabs that pick a chart.
---

```tsx
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>
  );
}
```

- Use metric cards for the key metrics of an overview. For numbers in a sidebar or in running text, use a [Property](/deft/components/property) or a [Stat](/deft/components/stat).
- Keep the `value` exact and the `label` short.

`Metric.Card` is the large form for the key metrics of an overview. Its `tone` follows the health of the metric and colours the delta; the card itself keeps a plain background, so charts inside it read the same in every state; put the health label, a [Dot](/deft/components/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 and the default `md` for the cards beside it; the delta always sits under the value. A card without `value` shows only its label and children, for a loading state.

## Cards with charts

```tsx
import { Chart, Dot, Metric, Sparkline } from "@openperf/deft";

import { Preview } from "../../src/Preview";

const VALUES = [40, 36, 48, 44, 32, 40, 52, 48, 60, 80, 100, 120];

const DATA = VALUES.map((value, index) => ({ time: index, value }));

export default function MetricChartCards() {
  return (
    <Preview block={true}>
      <div className="grid grid-cols-2 gap-3">
        <Metric.Card
          backdrop={<Sparkline.Root area={true} size="cover" tone="story" values={VALUES} />}
          delta="▲ 12%"
          label="Metric 1"
          status={<Dot.Root tone="info">Normal</Dot.Root>}
          unit="ms"
          value={120}
        />
        <Metric.Card
          backdrop={<Sparkline.Root area={true} size="cover" tone="warning" values={VALUES} />}
          delta="▲ 48%"
          label="Metric 2"
          status={<Dot.Root tone="warning">Degraded</Dot.Root>}
          tone="warning"
          unit="ms"
          value={120}
        />
      </div>
      <Metric.Card
        actions={
          <Chart.Legend
            items={[{ className: "bg-chart-bar", key: "value", label: "Series", value: 700 }]}
          />
        }
        label="Metric 3"
        size="lg"
        value={700}
      >
        <Chart.Root className="mt-4" data={DATA} keys={["value"]}>
          <Chart.Columns series={[{ className: "fill-chart-bar", key: "value" }]} />
        </Chart.Root>
      </Metric.Card>
    </Preview>
  );
}
```

A card is also the container of a chart. `actions` sits in the header before the status and takes the [Chart](/deft/components/chart) legend; the chart goes in children. For a small card, `backdrop` draws a `Sparkline` with `area` and `size="cover"` behind the value: it runs to the card edges and fades out towards the number.

## Tabs

```tsx
import { Dot, Metric, Sparkline } from "@openperf/deft";

import { Preview } from "../../src/Preview";

const TABS = [
  { name: "metric-1", label: "Metric 1", value: 72, values: [64, 70, 68, 74, 72, 76, 72] },
  { name: "metric-2", label: "Metric 2", value: 12, values: [10, 14, 12, 16, 12, 11, 12] },
  { name: "metric-3", label: "Metric 3", value: 64, values: [48, 52, 60, 58, 62, 66, 64] },
];

export default function MetricTabs() {
  return (
    <Preview block={true}>
      <Metric.Root defaultValue="metric-1">
        <Metric.List aria-label="Metrics" className="md:grid-cols-3">
          {TABS.map((tab) => (
            <Metric.Tab
              backdrop={<Sparkline.Root area={true} size="cover" values={tab.values} />}
              key={tab.name}
              label={tab.label}
              name={tab.name}
              status={<Dot.Root tone="info">Normal</Dot.Root>}
              value={tab.value}
            />
          ))}
        </Metric.List>
        {TABS.map((tab) => (
          <Metric.Panel key={tab.name} value={tab.name}>
            <Metric.Card label={tab.label} size="lg">
              Full chart of {tab.label}
            </Metric.Card>
          </Metric.Panel>
        ))}
      </Metric.Root>
    </Preview>
  );
}
```

When a page has a full chart for each of its headline numbers, make the cards tabs: `Metric.Tab` is a `Metric.Card` that selects the panel with the same `value` as its `name`, and the selected card carries an accent border. `Metric.Root`, `Metric.List` and `Metric.Panel` are the [Tabs](/deft/components/tabs) parts; `Metric.List` is a grid, so give it the columns. Put the full chart in a large card in each panel, without the value: the tab above already shows it.

## API

| Prop | Type | Default | Description |
| - | - | - | - |
| `name` | `string` | - | On Tab. Matches the `value` of its panel. |
| `label` | `ReactNode` | - | |
| `value?` | `ReactNode` | - | The number shown. |
| `unit?` | `ReactNode` | - | Follows the value in muted text. |
| `delta?` | `ReactNode` | - | |
| `status?` | `ReactNode` | - | |
| `tone?` | `"neutral" \| "success" \| "warning" \| "danger"` | `"neutral"` | |
| `actions?` | `ReactNode` | - | In the header, before the status. |
| `aside?` | `ReactNode` | - | |
| `backdrop?` | `ReactNode` | - | Drawn behind the value, faded towards it. |
| `size?` | `"md" \| "lg"` | `"md"` | |

Parts: `Card`, `Tab`, `List`, `Panel`, `Root`. `Root` and `Panel` are the [Tabs](/deft/components/tabs) parts. Built on Base UI [Tabs](https://base-ui.com/react/components/tabs).
