# Metric Card

A card for one KPI number, with a trend, a usage bar or a chart.

```tsx
import { MetricCard, MetricTrend, UsageBar } from 'ferry-ui'

export default function MetricCardHero() {
  return (
    <div className="grid gap-4 sm:grid-cols-2">
      <MetricCard label="Revenue" value="$48,200" trend={<MetricTrend>+12.5%</MetricTrend>} hint="Last 30 days" />
      <MetricCard
        label="Active users"
        value="3,912"
        trend={<MetricTrend direction="down">-2.1%</MetricTrend>}
        hint="Last 7 days"
      />
      <MetricCard label="Orders" value="1,284" hint="Last 30 days" />
      <MetricCard label="Storage" value="46 GB" unit="of 50 GB">
        <UsageBar value={92} label="Storage used" />
      </MetricCard>
    </div>
  )
}
```

## Usage guidelines

- **Numbers that people compare.** Use it for a revenue, a count, a rate or a usage.
- **Cards go in a grid.** Use `grid gap-4 sm:grid-cols-2 lg:grid-cols-4` for a row of four cards.
- **Format the value first.** The card shows `value` as you give it.
- **A fact is not a metric.** For an owner or the name of a plan, use [Info Tile](/docs/components/info-tile).
- **Not a container.** For other content, use [Card](/docs/components/card).

## Anatomy

Import the parts and put them together.

```tsx title="Anatomy"

<MetricCard label="" value="" trend={<MetricTrend />} aside={<LegendDot />}>
  <UsageBar value={0} label="" />
</MetricCard>
```

| Part | Role |
| --- | --- |
| `MetricCard` | The card: a label, a value and room for content below. |
| `MetricTrend` | A change, with an arrow and a color. It goes in `trend`. |
| `UsageBar` | A thin bar for a percentage from 0 to 100. |
| `LegendDot` | A dot and the name of a chart series. It goes in `aside`. |

## Examples

### Trend

`direction` sets the arrow of `MetricTrend`. `sentiment` sets the color. If an increase is bad, set `sentiment="negative"`.

```tsx
import { MetricCard, MetricTrend } from 'ferry-ui'

export default function MetricCardTrend() {
  return (
    <div className="grid gap-4 sm:grid-cols-3">
      <MetricCard label="Revenue" value="$48,200" trend={<MetricTrend direction="up">+12.5%</MetricTrend>} />
      <MetricCard label="Open tickets" value="14" trend={<MetricTrend direction="flat">0.0%</MetricTrend>} />
      <MetricCard
        label="Churn rate"
        value="3.1%"
        trend={
          <MetricTrend direction="up" sentiment="negative">
            +0.4 pts
          </MetricTrend>
        }
      />
    </div>
  )
}
```

| Direction | Default sentiment |
| --- | --- |
| `up` (default) | `positive`, green |
| `down` | `negative`, red |
| `flat` | `neutral`, muted |

### Usage bar

Put a `UsageBar` in the children of the card to show a quota. With the default `auto` tone, the bar changes to `warning` at 75 and to `destructive` at 90. `warningAt` and `destructiveAt` change these limits.

```tsx
import { MetricCard, UsageBar } from 'ferry-ui'

const QUOTAS = [
  { label: 'API calls', value: '320k', unit: 'of 1M', percent: 32 },
  { label: 'Seats', value: '8', unit: 'of 10', percent: 80 },
  { label: 'Storage', value: '47 GB', unit: 'of 50 GB', percent: 94 },
]

export default function MetricCardUsage() {
  return (
    <div className="grid gap-4 sm:grid-cols-3">
      {QUOTAS.map((quota) => (
        <MetricCard key={quota.label} label={quota.label} value={quota.value} unit={quota.unit}>
          <UsageBar value={quota.percent} label={`${quota.label} used`} />
        </MetricCard>
      ))}
    </div>
  )
}
```

### Info tooltip

`info` adds a button after the label. Its [Tooltip](/docs/components/tooltip) shows the text on hover and on keyboard focus. A touch screen cannot show it. Do not put necessary information there.

The tooltip needs the `TooltipProvider` of the app. See [Quick start](/docs/overview/quick-start).

```tsx
import { MetricCard } from 'ferry-ui'

export default function MetricCardInfo() {
  return (
    <MetricCard
      className="w-full max-w-xs"
      label="Error rate"
      value="0.42%"
      hint="Last 24 hours"
      info="The share of API requests that failed."
      infoLabel="About the error rate"
    />
  )
}
```

### Legend and chart

`aside` shows content on the right of the label. Use `LegendDot` there to name a chart series. Put the chart in the children.

```tsx
import { LegendDot, MetricCard } from 'ferry-ui'

// Requests for each hour, in thousands.
const REQUESTS = [42, 38, 31, 26, 22, 24, 35, 58, 74, 88, 92, 86, 95, 99, 93, 80, 64, 49]

export default function MetricCardLegend() {
  const peak = Math.max(...REQUESTS)

  return (
    <MetricCard
      className="w-full max-w-sm"
      label="API requests"
      value="1.1M"
      hint="Last 18 hours"
      aside={<LegendDot tone="brand">Requests</LegendDot>}
    >
      <div
        role="img"
        aria-label={`Requests for each hour. The peak is ${peak}k.`}
        className="flex h-12 items-end gap-1"
      >
        {REQUESTS.map((count, index) => (
          <span key={index} className="flex-1 rounded-sm bg-brand" style={{ height: `${(count / peak) * 100}%` }} />
        ))}
      </div>
    </MetricCard>
  )
}
```

### Loading

Set `loading` while the value loads. The card shows a skeleton and hides `unit`, `trend` and `hint`. The children stay. Give them their own empty state.

```tsx
import * as React from 'react'
import { Label, MetricCard, MetricTrend, Switch, UsageBar } from 'ferry-ui'

export default function MetricCardLoading() {
  // In an app, `loading` comes from the request that loads the numbers.
  const [loading, setLoading] = React.useState(true)

  return (
    <div className="flex flex-col gap-4">
      <div className="flex items-center gap-2">
        <Switch id="metrics-loading" checked={loading} onCheckedChange={setLoading} />
        <Label htmlFor="metrics-loading">Loading</Label>
      </div>
      <div className="grid gap-4 sm:grid-cols-2">
        <MetricCard
          label="Revenue"
          value="$48,200"
          trend={<MetricTrend>+12.5%</MetricTrend>}
          hint="Last 30 days"
          loading={loading}
        />
        <MetricCard label="Storage" value="46 GB" unit="of 50 GB" loading={loading}>
          <UsageBar value={loading ? null : 92} label="Storage used" />
        </MetricCard>
      </div>
    </div>
  )
}
```

### Compact

`compact` changes the padding from 20px to 16px. Use it for a dense grid of small cards.

```tsx
import { MetricCard } from 'ferry-ui'

export default function MetricCardCompact() {
  return (
    <div className="grid gap-4 sm:grid-cols-3">
      <MetricCard compact label="Open tickets" value="14" />
      <MetricCard compact label="First reply" value="1h 12m" />
      <MetricCard compact label="Satisfaction" value="96%" />
    </div>
  )
}
```

## Accessibility

- Write the sign in the text of `MetricTrend`, such as "+12.5%". The arrow is decorative, and the color alone does not give the meaning.
- `UsageBar` has the `meter` role. Give it a `label`.
- `infoLabel` names the info button. The default is "More info".

## API reference

`MetricCard` and `UsageBar` also accept each attribute of the `<div>` element. `MetricTrend` and `LegendDot` accept each attribute of the `<span>` element.

### MetricCard

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` (required) | `ReactNode` |  | Short caption rendered as a `MonoLabel` ("Revenue", "Active users", "Error rate"). |
| `value` | `ReactNode` |  | The number, already formatted ("12.4%", "$48,200", "1,284"; `0` is shown). Omit to render a card with only a label and `children`. |
| `unit` | `ReactNode` |  | Small muted text right after the value ("of 10", "/ 50 GB", "per month"). |
| `trend` | `ReactNode` |  | Change indicator after the value, usually a `MetricTrend` ("↗ +12.5%"). Shown only with a `value`. |
| `hint` | `ReactNode` |  | Secondary line under the value ("Last 30 days", "Updated 2 min ago"). Truncated to one line. |
| `info` | `ReactNode` |  | Explanation shown in a tooltip from an (i) button next to the label (hover or keyboard focus). Keep it to one sentence; requires a `TooltipProvider` ancestor. Not reachable on touch screens, so never put essential information here. |
| `infoLabel` | `string` | `More info` | Accessible name of the (i) info button. Defaults to "More info"; make it specific in dense grids. |
| `aside` | `ReactNode` |  | Right side of the header (12px muted text): a legend (`LegendDot`), a status badge or a small link. |
| `loading` | `boolean` | `false` | Shows a skeleton instead of the value, hides `unit`, `trend` and `hint`, and sets `aria-busy`. `children` still render, so give them their own empty or loading state. |
| `children` | `ReactNode` |  | Extra content under the value: a `UsageBar`, a sparkline, a chart or a breakdown. |
| `compact` | `boolean` | `false` | Tighter padding (16px instead of 20px) for dense grids of many small cards. |

### MetricTrend

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `direction` | `"flat" \| "up" \| "down"` | `up` | Arrow direction. Defaults to `up`. |
| `sentiment` | `"neutral" \| "positive" \| "negative"` |  | Color. Defaults to `positive` (green) for `up`, `negative` (red) for `down`, `neutral` (muted) for `flat`. Override it when a rise is bad (error rate, churn, latency): `direction="up" sentiment="negative"`. |
| `children` | `ReactNode` |  | The signed, formatted change ("+12.5%", "-3 pts"). It must make sense without the arrow. |

### UsageBar

`USAGE_BAR_TONES` is the list of the tones.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` (required) | `number \| null` |  | Percentage, 0–100 (clamped). `null`/`undefined`/`NaN` render an empty bar. |
| `tone` | `"auto" \| "destructive" \| "warning" \| "brand"` | `auto` | Fill color. Defaults to `auto` (threshold based). Force a tone only when thresholds do not apply. |
| `label` | `string` |  | Accessible name of the meter ("Storage used"). Always set it, or pass `aria-labelledby`. Add `aria-valuetext` ("46 of 50 GB") when the raw percentage is not what people read. |
| `warningAt` | `number` | `75` | Percentage at which the `auto` tone turns `warning`. Defaults to 75. |
| `destructiveAt` | `number` | `90` | Percentage at which the `auto` tone turns `destructive`. Defaults to 90. |

### LegendDot

`LEGEND_DOT_TONES` is the list of the tones.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `tone` | `"destructive" \| "warning" \| "success" \| "info" \| "neutral" \| "brand" \| "chart-1" \| "chart-2" \| "chart-3" \| "chart-4" \| "chart-5"` | `brand` | Dot color. Defaults to `brand`. Match it to the color of the series it labels. |
| `dotClassName` | `string` |  | Extra classes for the dot itself (another `bg-*` token class, `bg-brand/50`…). |
| `children` | `ReactNode` |  | Series name, 1–2 words. Rendered UPPERCASE. |
