> ## Documentation Index
> Fetch the complete documentation index at: https://prefab.prefect.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Metric

> Display a headline number with optional trend indicator for KPI dashboards.

export const ComponentPreview = ({json, height, resizable, bare, hideJson, playground, children}) => {
  const hostRef = React.useRef(null);
  const handleRef = React.useRef(null);
  const cardRef = React.useRef(null);
  const containerRef = React.useRef(null);
  const [previewWidth, setPreviewWidth] = React.useState(null);
  const [isDragging, setIsDragging] = React.useState(false);
  const [playgroundHref, setPlaygroundHref] = React.useState(null);
  React.useEffect(function () {
    if (!playground) return;
    var isLocal = location.hostname === "localhost" || location.hostname === "127.0.0.1";
    var path = isLocal ? "/playground" : "/docs/playground";
    var url = new URL(path, window.location.href);
    url.hash = "code=" + playground;
    setPlaygroundHref(url.href);
  }, [playground]);
  var jsonStr = typeof json === "string" ? json : JSON.stringify(json, null, 2);
  React.useEffect(function () {
    if (cardRef.current) {
      var card = cardRef.current.closest(".group");
      if (card) card.classList.remove("group");
    }
    var host = hostRef.current;
    if (!host || !json) return;
    function mount() {
      if (!window.__prefab || !host) return;
      var dark = document.documentElement.classList.contains("dark");
      handleRef.current = window.__prefab.mountPreview(host, jsonStr, {
        dark: dark
      });
      if (bare && host.shadowRoot) {
        var m = host.shadowRoot.querySelector("[data-prefab-mount]");
        if (m) m.style.background = "transparent";
      }
    }
    if (window.__prefab) {
      mount();
    } else {
      if (!window.__prefabLoading) {
        if (!window.__prefabReady) {
          var s = document.createElement("script");
          s.src = "/renderer.js";
          document.head.appendChild(s);
        }
        window.__prefabLoading = new Promise(function (resolve) {
          function check() {
            if (window.__prefabReady) {
              window.__prefabReady.then(resolve);
            } else {
              setTimeout(check, 10);
            }
          }
          check();
        });
      }
      window.__prefabLoading.then(mount);
    }
    var observer = new MutationObserver(function () {
      var dark = document.documentElement.classList.contains("dark");
      if (handleRef.current) handleRef.current.setDark(dark);
    });
    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ["class"]
    });
    return function () {
      observer.disconnect();
      if (handleRef.current) {
        handleRef.current.unmount();
        handleRef.current = null;
      }
    };
  }, [json]);
  function startResize(e) {
    e.preventDefault();
    var startX = e.clientX;
    var startW = hostRef.current ? hostRef.current.offsetWidth : 0;
    var maxW = containerRef.current ? containerRef.current.offsetWidth - 10 : startW;
    setIsDragging(true);
    function onMove(ev) {
      var w = Math.max(260, Math.min(startW + (ev.clientX - startX), maxW));
      setPreviewWidth(w);
    }
    function onUp() {
      document.removeEventListener("mousemove", onMove);
      document.removeEventListener("mouseup", onUp);
      setIsDragging(false);
    }
    document.addEventListener("mousemove", onMove);
    document.addEventListener("mouseup", onUp);
  }
  if (bare) {
    return <div ref={hostRef} />;
  }
  if (resizable) {
    return <Card>
        <div ref={cardRef} />
        <div ref={containerRef} style={{
      display: "flex",
      position: "relative",
      overflow: "hidden"
    }}>
          <div ref={hostRef} style={{
      flex: previewWidth ? "none" : 1,
      width: previewWidth ? previewWidth + "px" : undefined,
      minWidth: 0
    }} />
          <div onMouseDown={startResize} style={{
      width: "14px",
      flexShrink: 0,
      cursor: "col-resize",
      background: isDragging ? "var(--border, #e5e7eb)" : "color-mix(in srgb, var(--border, #e5e7eb) 40%, transparent)",
      display: "flex",
      alignItems: "center",
      justifyContent: "center",
      transition: "background 0.15s",
      userSelect: "none"
    }}>
            <svg width="6" height="24" style={{
      opacity: 0.5
    }}>
              <circle cx="2" cy="4" r="1.2" fill="currentColor" />
              <circle cx="2" cy="9" r="1.2" fill="currentColor" />
              <circle cx="2" cy="14" r="1.2" fill="currentColor" />
              <circle cx="2" cy="19" r="1.2" fill="currentColor" />
              <circle cx="5" cy="4" r="1.2" fill="currentColor" />
              <circle cx="5" cy="9" r="1.2" fill="currentColor" />
              <circle cx="5" cy="14" r="1.2" fill="currentColor" />
              <circle cx="5" cy="19" r="1.2" fill="currentColor" />
            </svg>
          </div>
          <div style={{
      flex: previewWidth ? 1 : "0 0 24px",
      minWidth: "24px",
      background: "repeating-linear-gradient(-45deg, transparent, transparent 3px, var(--border, #e5e7eb) 3px, var(--border, #e5e7eb) 4px)",
      opacity: 0.4
    }} />
          {previewWidth && <div style={{
      position: "absolute",
      bottom: "8px",
      right: "8px",
      background: "var(--background, white)",
      border: "1px solid var(--border, #e5e7eb)",
      borderRadius: "4px",
      padding: "1px 6px",
      fontSize: "11px",
      fontFamily: "monospace",
      color: "var(--muted-foreground, #6b7280)",
      pointerEvents: "none"
    }}>
              {previewWidth}px
            </div>}
        </div>
        {playgroundHref && <div style={{
      display: "flex",
      justifyContent: "flex-end",
      padding: "4px 0 8px"
    }}>
            <a href={playgroundHref} target="_blank" rel="noopener noreferrer" style={{
      fontSize: "11px",
      opacity: 0.4,
      textDecoration: "none",
      textDecorationLine: "none",
      borderBottom: "none",
      boxShadow: "none",
      display: "inline-flex",
      alignItems: "center",
      gap: "3px",
      transition: "opacity 0.15s",
      fontWeight: 400
    }} onMouseOver={function (e) {
      e.currentTarget.style.opacity = "0.6";
    }} onMouseOut={function (e) {
      e.currentTarget.style.opacity = "0.4";
    }}>
              Edit in Playground
              <svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M15 3h6v6" /><path d="M10 14 21 3" /><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></svg>
            </a>
          </div>}
        <div style={{
      marginBottom: "-2rem"
    }}>{children}</div>
      </Card>;
  }
  return <Card>
      <div ref={cardRef} />
      <div ref={hostRef} />
      {playgroundHref && <div style={{
    display: "flex",
    justifyContent: "flex-end",
    padding: "4px 0 8px"
  }}>
          <a href={playgroundHref} target="_blank" rel="noopener noreferrer" style={{
    fontSize: "11px",
    opacity: 0.4,
    textDecoration: "none",
    textDecorationLine: "none",
    borderBottom: "none",
    boxShadow: "none",
    display: "inline-flex",
    alignItems: "center",
    gap: "3px",
    transition: "opacity 0.15s",
    fontWeight: 400
  }} onMouseOver={function (e) {
    e.currentTarget.style.opacity = "0.6";
  }} onMouseOut={function (e) {
    e.currentTarget.style.opacity = "0.4";
  }}>
            Edit in Playground
            <svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M15 3h6v6" /><path d="M10 14 21 3" /><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></svg>
          </a>
        </div>}
      <div style={{
    marginBottom: "-2rem"
  }}>{children}</div>
    </Card>;
};

Metric is the dashboard primitive -- a big number with a label, and optionally a delta that shows how that number is trending. It handles the visual logic of trend arrows and sentiment colors automatically, so you can feed it raw data and get a polished KPI card without fiddling with icons or color classes. You provide the data; Metric provides the opinion about how it should look.

## Basic Usage

At minimum, a Metric needs a `label` and a `value`. The label names the statistic, and the value is the headline number that catches the eye. Values can be strings or raw numbers -- strings are displayed as-is, so you have full control over formatting.

When the value comes from state, use reactive expressions with [pipes](/docs/concepts/expressions#formatting-with-pipes) to format it at display time. Here the raw number `42000000` becomes `$42M` by dividing, rounding, and wrapping in literal `$` and `M` characters — no Python formatting needed.

<ComponentPreview json={{"view":{"cssClass":"pf-app-root","type":"Div","children":[{"type":"Metric","label":"Revenue","value":"${{ revenue / 1000000 | round:0 }}M"}]},"state":{"revenue":42000000}}} playground="ZnJvbSBwcmVmYWJfdWkuYXBwIGltcG9ydCBQcmVmYWJBcHAKZnJvbSBwcmVmYWJfdWkuY29tcG9uZW50cyBpbXBvcnQgU1RBVEUgYXMgc3RhdGUsIE1ldHJpYwoKd2l0aCBQcmVmYWJBcHAoc3RhdGU9eyJyZXZlbnVlIjogNDJfMDAwXzAwMH0pIGFzIGFwcDoKICAgIE1ldHJpYyhsYWJlbD0iUmV2ZW51ZSIsIHZhbHVlPWYiJHsoc3RhdGUucmV2ZW51ZSAvIDFfMDAwXzAwMCkucm91bmQoKX1NIikK">
  <CodeGroup>
    ```python Python icon="python" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    from prefab_ui.app import PrefabApp
    from prefab_ui.components import STATE as state, Metric

    with PrefabApp(state={"revenue": 42_000_000}) as app:
        Metric(label="Revenue", value=f"${(state.revenue / 1_000_000).round()}M")
    ```

    ```json Protocol icon="brackets-curly" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    {
      "view": {
        "cssClass": "pf-app-root",
        "type": "Div",
        "children": [
          {
            "type": "Metric",
            "label": "Revenue",
            "value": "${{ revenue / 1000000 | round:0 }}M"
          }
        ]
      },
      "state": {"revenue": 42000000}
    }
    ```
  </CodeGroup>
</ComponentPreview>

## Description

Add a `description` for extra context below the headline number. This is the right place for time ranges, data sources, percentile qualifiers, or any caveat that helps readers interpret the number correctly. Without it, a value like "142ms" could mean anything -- with it, the reader knows exactly what they're looking at.

<ComponentPreview json={{"view":{"type":"Metric","label":"Response Time","value":"142ms","description":"p95 over the last 24 hours"}}} playground="ZnJvbSBwcmVmYWJfdWkuY29tcG9uZW50cyBpbXBvcnQgTWV0cmljCgpNZXRyaWMoCiAgICBsYWJlbD0iUmVzcG9uc2UgVGltZSIsCiAgICB2YWx1ZT0iMTQybXMiLAogICAgZGVzY3JpcHRpb249InA5NSBvdmVyIHRoZSBsYXN0IDI0IGhvdXJzIiwKKQo">
  <CodeGroup>
    ```python Python icon="python" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    from prefab_ui.components import Metric

    Metric(
        label="Response Time",
        value="142ms",
        description="p95 over the last 24 hours",
    )
    ```

    ```json Protocol icon="brackets-curly" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    {
      "view": {
        "type": "Metric",
        "label": "Response Time",
        "value": "142ms",
        "description": "p95 over the last 24 hours"
      }
    }
    ```
  </CodeGroup>
</ComponentPreview>

## Trend Indicator

Pass a `delta` to show a change indicator alongside the value. Metric automatically infers the trend direction and color from the delta's sign: positive deltas get a green up arrow, negative deltas get a red down arrow.

<ComponentPreview json={{"view":{"cssClass":"gap-6","type":"Row","children":[{"type":"Card","children":[{"cssClass":"p-6","type":"CardContent","children":[{"type":"Metric","label":"Revenue","value":"$1.2M","delta":"+12%"}]}]},{"type":"Card","children":[{"cssClass":"p-6","type":"CardContent","children":[{"type":"Metric","label":"Units Sold","value":"8,430","delta":"-340"}]}]}]}}} playground="ZnJvbSBwcmVmYWJfdWkuY29tcG9uZW50cyBpbXBvcnQgQ2FyZCwgQ2FyZENvbnRlbnQsIE1ldHJpYywgUm93Cgp3aXRoIFJvdyhnYXA9Nik6CiAgICB3aXRoIENhcmQoKToKICAgICAgICB3aXRoIENhcmRDb250ZW50KGNzc19jbGFzcz0icC02Iik6CiAgICAgICAgICAgIE1ldHJpYyhsYWJlbD0iUmV2ZW51ZSIsIHZhbHVlPSIkMS4yTSIsIGRlbHRhPSIrMTIlIikKICAgIHdpdGggQ2FyZCgpOgogICAgICAgIHdpdGggQ2FyZENvbnRlbnQoY3NzX2NsYXNzPSJwLTYiKToKICAgICAgICAgICAgTWV0cmljKGxhYmVsPSJVbml0cyBTb2xkIiwgdmFsdWU9IjgsNDMwIiwgZGVsdGE9Ii0zNDAiKQo">
  <CodeGroup>
    ```python Python icon="python" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    from prefab_ui.components import Card, CardContent, Metric, Row

    with Row(gap=6):
        with Card():
            with CardContent(css_class="p-6"):
                Metric(label="Revenue", value="$1.2M", delta="+12%")
        with Card():
            with CardContent(css_class="p-6"):
                Metric(label="Units Sold", value="8,430", delta="-340")
    ```

    ```json Protocol icon="brackets-curly" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    {
      "view": {
        "cssClass": "gap-6",
        "type": "Row",
        "children": [
          {
            "type": "Card",
            "children": [
              {
                "cssClass": "p-6",
                "type": "CardContent",
                "children": [{"type": "Metric", "label": "Revenue", "value": "$1.2M", "delta": "+12%"}]
              }
            ]
          },
          {
            "type": "Card",
            "children": [
              {
                "cssClass": "p-6",
                "type": "CardContent",
                "children": [{"type": "Metric", "label": "Units Sold", "value": "8,430", "delta": "-340"}]
              }
            ]
          }
        ]
      }
    }
    ```
  </CodeGroup>
</ComponentPreview>

## Inverted Sentiment

The auto-inference assumes "up is good, down is bad," which works for most metrics but breaks for things like response time, error rates, or costs -- anything where going up is bad news. Override the automatic coloring by setting `trend_sentiment` explicitly. The arrow still points in the direction the number moved, but the color reflects whether that movement is good or bad.

Here response time increased by 18ms. Without the override, a positive delta would get a green arrow. Setting `trend_sentiment="negative"` makes it red, correctly signaling that slower is worse.

<ComponentPreview json={{"view":{"cssClass":"gap-6","type":"Row","children":[{"type":"Card","children":[{"cssClass":"p-6","type":"CardContent","children":[{"type":"Metric","label":"Revenue","value":"$1.2M","delta":"+12%"}]}]},{"type":"Card","children":[{"cssClass":"p-6","type":"CardContent","children":[{"type":"Metric","label":"Response Time","value":"284ms","delta":"+18ms","trend":"up","trendSentiment":"negative"}]}]}]}}} playground="ZnJvbSBwcmVmYWJfdWkuY29tcG9uZW50cyBpbXBvcnQgQ2FyZCwgQ2FyZENvbnRlbnQsIE1ldHJpYywgUm93Cgp3aXRoIFJvdyhnYXA9Nik6CiAgICB3aXRoIENhcmQoKToKICAgICAgICB3aXRoIENhcmRDb250ZW50KGNzc19jbGFzcz0icC02Iik6CiAgICAgICAgICAgIE1ldHJpYyhsYWJlbD0iUmV2ZW51ZSIsIHZhbHVlPSIkMS4yTSIsIGRlbHRhPSIrMTIlIikKICAgIHdpdGggQ2FyZCgpOgogICAgICAgIHdpdGggQ2FyZENvbnRlbnQoY3NzX2NsYXNzPSJwLTYiKToKICAgICAgICAgICAgTWV0cmljKAogICAgICAgICAgICAgICAgbGFiZWw9IlJlc3BvbnNlIFRpbWUiLAogICAgICAgICAgICAgICAgdmFsdWU9IjI4NG1zIiwKICAgICAgICAgICAgICAgIGRlbHRhPSIrMThtcyIsCiAgICAgICAgICAgICAgICB0cmVuZD0idXAiLAogICAgICAgICAgICAgICAgdHJlbmRfc2VudGltZW50PSJuZWdhdGl2ZSIsCiAgICAgICAgICAgICkK">
  <CodeGroup>
    ```python Python {10-11} icon="python" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    from prefab_ui.components import Card, CardContent, Metric, Row

    with Row(gap=6):
        with Card():
            with CardContent(css_class="p-6"):
                Metric(label="Revenue", value="$1.2M", delta="+12%")
        with Card():
            with CardContent(css_class="p-6"):
                Metric(
                    label="Response Time",
                    value="284ms",
                    delta="+18ms",
                    trend="up",
                    trend_sentiment="negative",
                )
    ```

    ```json Protocol icon="brackets-curly" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    {
      "view": {
        "cssClass": "gap-6",
        "type": "Row",
        "children": [
          {
            "type": "Card",
            "children": [
              {
                "cssClass": "p-6",
                "type": "CardContent",
                "children": [{"type": "Metric", "label": "Revenue", "value": "$1.2M", "delta": "+12%"}]
              }
            ]
          },
          {
            "type": "Card",
            "children": [
              {
                "cssClass": "p-6",
                "type": "CardContent",
                "children": [
                  {
                    "type": "Metric",
                    "label": "Response Time",
                    "value": "284ms",
                    "delta": "+18ms",
                    "trend": "up",
                    "trendSentiment": "negative"
                  }
                ]
              }
            ]
          }
        ]
      }
    }
    ```
  </CodeGroup>
</ComponentPreview>

## KPI Row

The classic dashboard header: a row of Metric cards spanning the top of a page. Wrap each Metric in a Card for visual separation and use a Grid to give every card equal width. This pattern scales naturally -- add or remove cards and the grid reflows without manual width calculations.

<ComponentPreview json={{"view":{"cssClass":"gap-4","type":"Grid","minColumnWidth":"14rem","children":[{"type":"Card","children":[{"cssClass":"p-6","type":"CardContent","children":[{"type":"Metric","label":"Revenue","value":"$42M","description":"Year to date","delta":"+12%"}]}]},{"type":"Card","children":[{"cssClass":"p-6","type":"CardContent","children":[{"type":"Metric","label":"Conversion","value":"3.2%","description":"Last 30 days","delta":"+0.4%"}]}]},{"type":"Card","children":[{"cssClass":"p-6","type":"CardContent","children":[{"type":"Metric","label":"Avg. Order","value":"$128","description":"Per transaction","delta":"-$3","trend":"down","trendSentiment":"negative"}]}]}]}}} playground="ZnJvbSBwcmVmYWJfdWkuY29tcG9uZW50cyBpbXBvcnQgQ2FyZCwgQ2FyZENvbnRlbnQsIEdyaWQsIE1ldHJpYwoKd2l0aCBHcmlkKG1pbl9jb2x1bW5fd2lkdGg9IjE0cmVtIiwgZ2FwPTQpOgogICAgd2l0aCBDYXJkKCk6CiAgICAgICAgd2l0aCBDYXJkQ29udGVudChjc3NfY2xhc3M9InAtNiIpOgogICAgICAgICAgICBNZXRyaWMoCiAgICAgICAgICAgICAgICBsYWJlbD0iUmV2ZW51ZSIsCiAgICAgICAgICAgICAgICB2YWx1ZT0iJDQyTSIsCiAgICAgICAgICAgICAgICBkZXNjcmlwdGlvbj0iWWVhciB0byBkYXRlIiwKICAgICAgICAgICAgICAgIGRlbHRhPSIrMTIlIiwKICAgICAgICAgICAgKQoKICAgIHdpdGggQ2FyZCgpOgogICAgICAgIHdpdGggQ2FyZENvbnRlbnQoY3NzX2NsYXNzPSJwLTYiKToKICAgICAgICAgICAgTWV0cmljKAogICAgICAgICAgICAgICAgbGFiZWw9IkNvbnZlcnNpb24iLAogICAgICAgICAgICAgICAgdmFsdWU9IjMuMiUiLAogICAgICAgICAgICAgICAgZGVzY3JpcHRpb249Ikxhc3QgMzAgZGF5cyIsCiAgICAgICAgICAgICAgICBkZWx0YT0iKzAuNCUiLAogICAgICAgICAgICApCgogICAgd2l0aCBDYXJkKCk6CiAgICAgICAgd2l0aCBDYXJkQ29udGVudChjc3NfY2xhc3M9InAtNiIpOgogICAgICAgICAgICBNZXRyaWMoCiAgICAgICAgICAgICAgICBsYWJlbD0iQXZnLiBPcmRlciIsCiAgICAgICAgICAgICAgICB2YWx1ZT0iJDEyOCIsCiAgICAgICAgICAgICAgICBkZXNjcmlwdGlvbj0iUGVyIHRyYW5zYWN0aW9uIiwKICAgICAgICAgICAgICAgIGRlbHRhPSItJDMiLAogICAgICAgICAgICAgICAgdHJlbmQ9ImRvd24iLAogICAgICAgICAgICAgICAgdHJlbmRfc2VudGltZW50PSJuZWdhdGl2ZSIsCiAgICAgICAgICAgICkK">
  <CodeGroup>
    ```python Python expandable icon="python" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    from prefab_ui.components import Card, CardContent, Grid, Metric

    with Grid(min_column_width="14rem", gap=4):
        with Card():
            with CardContent(css_class="p-6"):
                Metric(
                    label="Revenue",
                    value="$42M",
                    description="Year to date",
                    delta="+12%",
                )

        with Card():
            with CardContent(css_class="p-6"):
                Metric(
                    label="Conversion",
                    value="3.2%",
                    description="Last 30 days",
                    delta="+0.4%",
                )

        with Card():
            with CardContent(css_class="p-6"):
                Metric(
                    label="Avg. Order",
                    value="$128",
                    description="Per transaction",
                    delta="-$3",
                    trend="down",
                    trend_sentiment="negative",
                )
    ```

    ```json Protocol icon="brackets-curly" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    {
      "view": {
        "cssClass": "gap-4",
        "type": "Grid",
        "minColumnWidth": "14rem",
        "children": [
          {
            "type": "Card",
            "children": [
              {
                "cssClass": "p-6",
                "type": "CardContent",
                "children": [
                  {
                    "type": "Metric",
                    "label": "Revenue",
                    "value": "$42M",
                    "description": "Year to date",
                    "delta": "+12%"
                  }
                ]
              }
            ]
          },
          {
            "type": "Card",
            "children": [
              {
                "cssClass": "p-6",
                "type": "CardContent",
                "children": [
                  {
                    "type": "Metric",
                    "label": "Conversion",
                    "value": "3.2%",
                    "description": "Last 30 days",
                    "delta": "+0.4%"
                  }
                ]
              }
            ]
          },
          {
            "type": "Card",
            "children": [
              {
                "cssClass": "p-6",
                "type": "CardContent",
                "children": [
                  {
                    "type": "Metric",
                    "label": "Avg. Order",
                    "value": "$128",
                    "description": "Per transaction",
                    "delta": "-$3",
                    "trend": "down",
                    "trendSentiment": "negative"
                  }
                ]
              }
            ]
          }
        ]
      }
    }
    ```
  </CodeGroup>
</ComponentPreview>

## Dashboard Layout

Metrics work as the top row of a dashboard, with a chart below for detail. Use `min_column_width` on the Grid so the metric cards reflow naturally at any container width.

<ComponentPreview json={{"view":{"cssClass":"gap-4","type":"Column","children":[{"cssClass":"gap-4","type":"Grid","minColumnWidth":"14rem","children":[{"type":"Card","children":[{"cssClass":"p-6","type":"CardContent","children":[{"type":"Metric","label":"Revenue","value":"$1.52M","description":"Q2 2025 total","delta":"+12%"}]}]},{"type":"Card","children":[{"cssClass":"p-6","type":"CardContent","children":[{"type":"Metric","label":"Active Users","value":"26,104","description":"Monthly active","delta":"+4.3%"}]}]},{"type":"Card","children":[{"cssClass":"p-6","type":"CardContent","children":[{"type":"Metric","label":"Infra Costs","value":"$48K","description":"vs. last month","delta":"-15%","trend":"down","trendSentiment":"positive"}]}]}]},{"type":"Card","children":[{"type":"CardHeader","children":[{"type":"CardTitle","content":"Monthly Revenue"}]},{"type":"CardContent","children":[{"type":"BarChart","data":[{"month":"Jan","revenue":186000},{"month":"Feb","revenue":305000},{"month":"Mar","revenue":237000},{"month":"Apr","revenue":273000},{"month":"May","revenue":209000},{"month":"Jun","revenue":314000}],"series":[{"dataKey":"revenue","label":"Revenue"}],"xAxis":"month","height":280,"stacked":false,"horizontal":false,"barRadius":4,"showLegend":true,"showTooltip":true,"animate":true,"showGrid":true,"showYAxis":true,"valueFormat":"auto"}]}]}]}}} playground="ZnJvbSBwcmVmYWJfdWkuY29tcG9uZW50cyBpbXBvcnQgQ2FyZCwgQ2FyZENvbnRlbnQsIENhcmRIZWFkZXIsIENhcmRUaXRsZSwgQ29sdW1uLCBHcmlkLCBNZXRyaWMKZnJvbSBwcmVmYWJfdWkuY29tcG9uZW50cy5jaGFydHMgaW1wb3J0IEJhckNoYXJ0LCBDaGFydFNlcmllcwoKZGF0YSA9IFsKICAgIHsibW9udGgiOiAiSmFuIiwgInJldmVudWUiOiAxODYwMDB9LAogICAgeyJtb250aCI6ICJGZWIiLCAicmV2ZW51ZSI6IDMwNTAwMH0sCiAgICB7Im1vbnRoIjogIk1hciIsICJyZXZlbnVlIjogMjM3MDAwfSwKICAgIHsibW9udGgiOiAiQXByIiwgInJldmVudWUiOiAyNzMwMDB9LAogICAgeyJtb250aCI6ICJNYXkiLCAicmV2ZW51ZSI6IDIwOTAwMH0sCiAgICB7Im1vbnRoIjogIkp1biIsICJyZXZlbnVlIjogMzE0MDAwfSwKXQoKd2l0aCBDb2x1bW4oZ2FwPTQpOgogICAgd2l0aCBHcmlkKG1pbl9jb2x1bW5fd2lkdGg9IjE0cmVtIiwgZ2FwPTQpOgogICAgICAgIHdpdGggQ2FyZCgpOgogICAgICAgICAgICB3aXRoIENhcmRDb250ZW50KGNzc19jbGFzcz0icC02Iik6CiAgICAgICAgICAgICAgICBNZXRyaWMoCiAgICAgICAgICAgICAgICAgICAgbGFiZWw9IlJldmVudWUiLAogICAgICAgICAgICAgICAgICAgIHZhbHVlPSIkMS41Mk0iLAogICAgICAgICAgICAgICAgICAgIGRlc2NyaXB0aW9uPSJRMiAyMDI1IHRvdGFsIiwKICAgICAgICAgICAgICAgICAgICBkZWx0YT0iKzEyJSIsCiAgICAgICAgICAgICAgICApCiAgICAgICAgd2l0aCBDYXJkKCk6CiAgICAgICAgICAgIHdpdGggQ2FyZENvbnRlbnQoY3NzX2NsYXNzPSJwLTYiKToKICAgICAgICAgICAgICAgIE1ldHJpYygKICAgICAgICAgICAgICAgICAgICBsYWJlbD0iQWN0aXZlIFVzZXJzIiwKICAgICAgICAgICAgICAgICAgICB2YWx1ZT0iMjYsMTA0IiwKICAgICAgICAgICAgICAgICAgICBkZXNjcmlwdGlvbj0iTW9udGhseSBhY3RpdmUiLAogICAgICAgICAgICAgICAgICAgIGRlbHRhPSIrNC4zJSIsCiAgICAgICAgICAgICAgICApCiAgICAgICAgd2l0aCBDYXJkKCk6CiAgICAgICAgICAgIHdpdGggQ2FyZENvbnRlbnQoY3NzX2NsYXNzPSJwLTYiKToKICAgICAgICAgICAgICAgIE1ldHJpYygKICAgICAgICAgICAgICAgICAgICBsYWJlbD0iSW5mcmEgQ29zdHMiLAogICAgICAgICAgICAgICAgICAgIHZhbHVlPSIkNDhLIiwKICAgICAgICAgICAgICAgICAgICBkZXNjcmlwdGlvbj0idnMuIGxhc3QgbW9udGgiLAogICAgICAgICAgICAgICAgICAgIGRlbHRhPSItMTUlIiwKICAgICAgICAgICAgICAgICAgICB0cmVuZD0iZG93biIsCiAgICAgICAgICAgICAgICAgICAgdHJlbmRfc2VudGltZW50PSJwb3NpdGl2ZSIsCiAgICAgICAgICAgICAgICApCiAgICB3aXRoIENhcmQoKToKICAgICAgICB3aXRoIENhcmRIZWFkZXIoKToKICAgICAgICAgICAgQ2FyZFRpdGxlKCJNb250aGx5IFJldmVudWUiKQogICAgICAgIHdpdGggQ2FyZENvbnRlbnQoKToKICAgICAgICAgICAgQmFyQ2hhcnQoCiAgICAgICAgICAgICAgICBkYXRhPWRhdGEsCiAgICAgICAgICAgICAgICBzZXJpZXM9W0NoYXJ0U2VyaWVzKGRhdGFfa2V5PSJyZXZlbnVlIiwgbGFiZWw9IlJldmVudWUiKV0sCiAgICAgICAgICAgICAgICB4X2F4aXM9Im1vbnRoIiwKICAgICAgICAgICAgICAgIGhlaWdodD0yODAsCiAgICAgICAgICAgICkK">
  <CodeGroup>
    ```python Python expandable icon="python" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    from prefab_ui.components import Card, CardContent, CardHeader, CardTitle, Column, Grid, Metric
    from prefab_ui.components.charts import BarChart, ChartSeries

    data = [
        {"month": "Jan", "revenue": 186000},
        {"month": "Feb", "revenue": 305000},
        {"month": "Mar", "revenue": 237000},
        {"month": "Apr", "revenue": 273000},
        {"month": "May", "revenue": 209000},
        {"month": "Jun", "revenue": 314000},
    ]

    with Column(gap=4):
        with Grid(min_column_width="14rem", gap=4):
            with Card():
                with CardContent(css_class="p-6"):
                    Metric(
                        label="Revenue",
                        value="$1.52M",
                        description="Q2 2025 total",
                        delta="+12%",
                    )
            with Card():
                with CardContent(css_class="p-6"):
                    Metric(
                        label="Active Users",
                        value="26,104",
                        description="Monthly active",
                        delta="+4.3%",
                    )
            with Card():
                with CardContent(css_class="p-6"):
                    Metric(
                        label="Infra Costs",
                        value="$48K",
                        description="vs. last month",
                        delta="-15%",
                        trend="down",
                        trend_sentiment="positive",
                    )
        with Card():
            with CardHeader():
                CardTitle("Monthly Revenue")
            with CardContent():
                BarChart(
                    data=data,
                    series=[ChartSeries(data_key="revenue", label="Revenue")],
                    x_axis="month",
                    height=280,
                )
    ```

    ```json Protocol icon="brackets-curly" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    {
      "view": {
        "cssClass": "gap-4",
        "type": "Column",
        "children": [
          {
            "cssClass": "gap-4",
            "type": "Grid",
            "minColumnWidth": "14rem",
            "children": [
              {
                "type": "Card",
                "children": [
                  {
                    "cssClass": "p-6",
                    "type": "CardContent",
                    "children": [
                      {
                        "type": "Metric",
                        "label": "Revenue",
                        "value": "$1.52M",
                        "description": "Q2 2025 total",
                        "delta": "+12%"
                      }
                    ]
                  }
                ]
              },
              {
                "type": "Card",
                "children": [
                  {
                    "cssClass": "p-6",
                    "type": "CardContent",
                    "children": [
                      {
                        "type": "Metric",
                        "label": "Active Users",
                        "value": "26,104",
                        "description": "Monthly active",
                        "delta": "+4.3%"
                      }
                    ]
                  }
                ]
              },
              {
                "type": "Card",
                "children": [
                  {
                    "cssClass": "p-6",
                    "type": "CardContent",
                    "children": [
                      {
                        "type": "Metric",
                        "label": "Infra Costs",
                        "value": "$48K",
                        "description": "vs. last month",
                        "delta": "-15%",
                        "trend": "down",
                        "trendSentiment": "positive"
                      }
                    ]
                  }
                ]
              }
            ]
          },
          {
            "type": "Card",
            "children": [
              {
                "type": "CardHeader",
                "children": [{"type": "CardTitle", "content": "Monthly Revenue"}]
              },
              {
                "type": "CardContent",
                "children": [
                  {
                    "type": "BarChart",
                    "data": [
                      {"month": "Jan", "revenue": 186000},
                      {"month": "Feb", "revenue": 305000},
                      {"month": "Mar", "revenue": 237000},
                      {"month": "Apr", "revenue": 273000},
                      {"month": "May", "revenue": 209000},
                      {"month": "Jun", "revenue": 314000}
                    ],
                    "series": [{"dataKey": "revenue", "label": "Revenue"}],
                    "xAxis": "month",
                    "height": 280,
                    "stacked": false,
                    "horizontal": false,
                    "barRadius": 4,
                    "showLegend": true,
                    "showTooltip": true,
                    "animate": true,
                    "showGrid": true,
                    "showYAxis": true,
                    "valueFormat": "auto"
                  }
                ]
              }
            ]
          }
        ]
      }
    }
    ```
  </CodeGroup>
</ComponentPreview>

## With Sparklines

Place a [Sparkline](/docs/components/sparkline) as a sibling to CardContent inside the Card. Use `pb-0 gap-0` on the Card to eliminate spacing below the metric, so the sparkline bleeds flush to the bottom edge.

<ComponentPreview json={{"view":{"cssClass":"gap-4","type":"Grid","minColumnWidth":"14rem","children":[{"cssClass":"pb-0 gap-0","type":"Card","children":[{"type":"CardContent","children":[{"type":"Metric","label":"Revenue","value":"$1.52M","delta":"+12%"}]},{"cssClass":"h-16","type":"Sparkline","data":[120,132,101,134,90,230,210,182,191,234,290,330],"variant":"success","fill":true,"curve":"linear","strokeWidth":1.5}]},{"cssClass":"pb-0 gap-0","type":"Card","children":[{"type":"CardContent","children":[{"type":"Metric","label":"Response Time","value":"284ms","delta":"+18ms","trend":"up","trendSentiment":"negative"}]},{"cssClass":"h-16","type":"Sparkline","data":[180,200,220,205,240,260,250,275,290,284],"variant":"destructive","fill":true,"curve":"linear","strokeWidth":1.5}]},{"cssClass":"pb-0 gap-0","type":"Card","children":[{"type":"CardContent","children":[{"type":"Metric","label":"Active Users","value":"26,104","delta":"+4.3%"}]},{"cssClass":"h-16","type":"Sparkline","data":[22,23,21,24,23,25,24,26,25,26],"variant":"info","fill":true,"curve":"linear","strokeWidth":1.5}]}]}}} playground="ZnJvbSBwcmVmYWJfdWkuY29tcG9uZW50cyBpbXBvcnQgQ2FyZCwgQ2FyZENvbnRlbnQsIEdyaWQKZnJvbSBwcmVmYWJfdWkuY29tcG9uZW50cy5jaGFydHMgaW1wb3J0IFNwYXJrbGluZQpmcm9tIHByZWZhYl91aS5jb21wb25lbnRzIGltcG9ydCBNZXRyaWMKCndpdGggR3JpZChtaW5fY29sdW1uX3dpZHRoPSIxNHJlbSIsIGdhcD00KToKICAgIHdpdGggQ2FyZChjc3NfY2xhc3M9InBiLTAgZ2FwLTAiKToKICAgICAgICB3aXRoIENhcmRDb250ZW50KCk6CiAgICAgICAgICAgIE1ldHJpYyhsYWJlbD0iUmV2ZW51ZSIsIHZhbHVlPSIkMS41Mk0iLCBkZWx0YT0iKzEyJSIpCiAgICAgICAgU3BhcmtsaW5lKAogICAgICAgICAgICBkYXRhPVsxMjAsIDEzMiwgMTAxLCAxMzQsIDkwLCAyMzAsIDIxMCwgMTgyLCAxOTEsIDIzNCwgMjkwLCAzMzBdLAogICAgICAgICAgICB2YXJpYW50PSJzdWNjZXNzIiwgZmlsbD1UcnVlLCBjc3NfY2xhc3M9ImgtMTYiLAogICAgICAgICkKICAgIHdpdGggQ2FyZChjc3NfY2xhc3M9InBiLTAgZ2FwLTAiKToKICAgICAgICB3aXRoIENhcmRDb250ZW50KCk6CiAgICAgICAgICAgIE1ldHJpYygKICAgICAgICAgICAgICAgIGxhYmVsPSJSZXNwb25zZSBUaW1lIiwgdmFsdWU9IjI4NG1zIiwKICAgICAgICAgICAgICAgIGRlbHRhPSIrMThtcyIsIHRyZW5kPSJ1cCIsIHRyZW5kX3NlbnRpbWVudD0ibmVnYXRpdmUiLAogICAgICAgICAgICApCiAgICAgICAgU3BhcmtsaW5lKAogICAgICAgICAgICBkYXRhPVsxODAsIDIwMCwgMjIwLCAyMDUsIDI0MCwgMjYwLCAyNTAsIDI3NSwgMjkwLCAyODRdLAogICAgICAgICAgICB2YXJpYW50PSJkZXN0cnVjdGl2ZSIsIGZpbGw9VHJ1ZSwgY3NzX2NsYXNzPSJoLTE2IiwKICAgICAgICApCiAgICB3aXRoIENhcmQoY3NzX2NsYXNzPSJwYi0wIGdhcC0wIik6CiAgICAgICAgd2l0aCBDYXJkQ29udGVudCgpOgogICAgICAgICAgICBNZXRyaWMobGFiZWw9IkFjdGl2ZSBVc2VycyIsIHZhbHVlPSIyNiwxMDQiLCBkZWx0YT0iKzQuMyUiKQogICAgICAgIFNwYXJrbGluZSgKICAgICAgICAgICAgZGF0YT1bMjIsIDIzLCAyMSwgMjQsIDIzLCAyNSwgMjQsIDI2LCAyNSwgMjZdLAogICAgICAgICAgICB2YXJpYW50PSJpbmZvIiwgZmlsbD1UcnVlLCBjc3NfY2xhc3M9ImgtMTYiLAogICAgICAgICkK">
  <CodeGroup>
    ```python Python expandable icon="python" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    from prefab_ui.components import Card, CardContent, Grid
    from prefab_ui.components.charts import Sparkline
    from prefab_ui.components import Metric

    with Grid(min_column_width="14rem", gap=4):
        with Card(css_class="pb-0 gap-0"):
            with CardContent():
                Metric(label="Revenue", value="$1.52M", delta="+12%")
            Sparkline(
                data=[120, 132, 101, 134, 90, 230, 210, 182, 191, 234, 290, 330],
                variant="success", fill=True, css_class="h-16",
            )
        with Card(css_class="pb-0 gap-0"):
            with CardContent():
                Metric(
                    label="Response Time", value="284ms",
                    delta="+18ms", trend="up", trend_sentiment="negative",
                )
            Sparkline(
                data=[180, 200, 220, 205, 240, 260, 250, 275, 290, 284],
                variant="destructive", fill=True, css_class="h-16",
            )
        with Card(css_class="pb-0 gap-0"):
            with CardContent():
                Metric(label="Active Users", value="26,104", delta="+4.3%")
            Sparkline(
                data=[22, 23, 21, 24, 23, 25, 24, 26, 25, 26],
                variant="info", fill=True, css_class="h-16",
            )
    ```

    ```json Protocol icon="brackets-curly" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    {
      "view": {
        "cssClass": "gap-4",
        "type": "Grid",
        "minColumnWidth": "14rem",
        "children": [
          {
            "cssClass": "pb-0 gap-0",
            "type": "Card",
            "children": [
              {
                "type": "CardContent",
                "children": [{"type": "Metric", "label": "Revenue", "value": "$1.52M", "delta": "+12%"}]
              },
              {
                "cssClass": "h-16",
                "type": "Sparkline",
                "data": [120, 132, 101, 134, 90, 230, 210, 182, 191, 234, 290, 330],
                "variant": "success",
                "fill": true,
                "curve": "linear",
                "strokeWidth": 1.5
              }
            ]
          },
          {
            "cssClass": "pb-0 gap-0",
            "type": "Card",
            "children": [
              {
                "type": "CardContent",
                "children": [
                  {
                    "type": "Metric",
                    "label": "Response Time",
                    "value": "284ms",
                    "delta": "+18ms",
                    "trend": "up",
                    "trendSentiment": "negative"
                  }
                ]
              },
              {
                "cssClass": "h-16",
                "type": "Sparkline",
                "data": [180, 200, 220, 205, 240, 260, 250, 275, 290, 284],
                "variant": "destructive",
                "fill": true,
                "curve": "linear",
                "strokeWidth": 1.5
              }
            ]
          },
          {
            "cssClass": "pb-0 gap-0",
            "type": "Card",
            "children": [
              {
                "type": "CardContent",
                "children": [
                  {"type": "Metric", "label": "Active Users", "value": "26,104", "delta": "+4.3%"}
                ]
              },
              {
                "cssClass": "h-16",
                "type": "Sparkline",
                "data": [22, 23, 21, 24, 23, 25, 24, 26, 25, 26],
                "variant": "info",
                "fill": true,
                "curve": "linear",
                "strokeWidth": 1.5
              }
            ]
          }
        ]
      }
    }
    ```
  </CodeGroup>
</ComponentPreview>

## API Reference

<Card icon="code" title="Metric Parameters">
  <ParamField body="label" type="str" required>
    The metric name displayed above the value (e.g. `"Revenue"`, `"Active Users"`).
  </ParamField>

  <ParamField body="value" type="str | int | float" required>
    The headline number. Strings are displayed as-is, so pre-format with currency symbols, commas, or units as needed.
  </ParamField>

  <ParamField body="description" type="str | None" default="None">
    Optional text below the value for additional context like time ranges or data sources.
  </ParamField>

  <ParamField body="delta" type="str | int | float | None" default="None">
    Change indicator shown next to the value (e.g. `"+23.4%"`, `"-15"`, `"+$2.3M"`). When present, an arrow icon and sentiment color are automatically inferred from the sign.
  </ParamField>

  <ParamField body="trend" type="str | None" default="None">
    Arrow direction: `"up"`, `"down"`, or `"neutral"`. Inferred from `delta` when omitted -- positive deltas point up, negative point down.
  </ParamField>

  <ParamField body="trend_sentiment" type="str | None" default="None">
    Color control: `"positive"` (green), `"negative"` (red), or `"neutral"` (muted). Inferred from `trend` when omitted -- up is positive, down is negative. Override this for inverted metrics where down is good (e.g. costs decreasing).
  </ParamField>

  <ParamField body="css_class" type="str | None" default="None">
    Additional Tailwind CSS classes.
  </ParamField>
</Card>

## Protocol Reference

```json Metric theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
{
  "type": "Metric",
  "id?": "string",
  "onMount?": "any",
  "label": "string (required)",
  "value": "number | string (required)",
  "description?": "string",
  "delta?": "number | string",
  "trend?": "up | down | neutral",
  "trendSentiment?": "positive | negative | neutral",
  "cssClass?": "string"
}
```

For the complete protocol schema, see [Metric](/docs/protocol/metric).
