# Meter group

Shows several amounts that share one range, such as the parts of a storage quota, side by side on one track.

- **Import:** `import { MeterGroup, MeterGroupMeters, MeterGroupLegend } from "@booleanpress/ui/meter-group"`
- **APG Meter:** <https://www.w3.org/WAI/ARIA/apg/patterns/meter/>
- **Page:** <https://ui.booleanpress.com/components/meter-group> · @booleanpress/ui 0.2.0

## Usage

Give the segments in `values`, each with a `label` and a `value`, and name the group.

```tsx
import { MeterGroup } from "@booleanpress/ui/meter-group"

export function Storage() {
  return (
    <MeterGroup
      aria-label="Storage by type"
      values={[
        { label: "Attachments", value: 16 },
        { label: "Logs", value: 8 },
      ]}
    />
  )
}
```

A segment's share of the track is its `value` over `max - min` (0 to 100 by default); set `max={200}` for a 200 GB quota. Segments that add up to more than the range stop at the end of the track, the later ones cut short, while each meter still reads its own value. Each segment takes the chart colours in turn unless its `color` names one, such as `"var(--success)"`; an `icon` replaces its dot in the legend. `formatValue(value, percent)` writes the value for the legend and for screen readers, a percentage in the provider's locale by default.

`labelPosition` puts the legend after the track (`end`) or before it (`start`), and `labelOrientation` lists it in a row or a column. `orientation="vertical"` stands the track up beside the legend; give the group a height. For a legend of your own, pass children: `MeterGroupMeters` draws the track and `MeterGroupLegend` the built-in legend, anywhere among your own content.

## Examples

### Basic

One segment: the space a site's email uses.

```tsx
import { MeterGroup } from "@booleanpress/ui/meter-group"

export default function MeterGroupBasic() {
  return <MeterGroup aria-label="Storage" values={[{ label: "Space used", value: 15 }]} className="w-full max-w-md" />
}
```

### Multiple

Four kinds of storage share the track, each in a chart colour, with a legend.

```tsx
import { MeterGroup } from "@booleanpress/ui/meter-group"

const STORAGE = [
  { label: "Attachments", value: 16, color: "var(--chart-5)" },
  { label: "Logs", value: 8, color: "var(--chart-2)" },
  { label: "Templates", value: 24, color: "var(--chart-4)" },
  { label: "Backups", value: 10, color: "var(--chart-3)" },
]

export default function MeterGroupMultiple() {
  return <MeterGroup aria-label="Storage by type" values={STORAGE} className="w-full max-w-md" />
}
```

### Icons

An `icon` on each segment replaces its dot in the legend, in the segment's colour.

```tsx
import { DatabaseBackupIcon, FileTextIcon, LayoutTemplateIcon, PaperclipIcon } from "lucide-react"
import { MeterGroup } from "@booleanpress/ui/meter-group"

const STORAGE = [
  { label: "Attachments", value: 16, color: "var(--chart-5)", icon: <PaperclipIcon /> },
  { label: "Logs", value: 8, color: "var(--chart-2)", icon: <FileTextIcon /> },
  { label: "Templates", value: 24, color: "var(--chart-4)", icon: <LayoutTemplateIcon /> },
  { label: "Backups", value: 10, color: "var(--chart-3)", icon: <DatabaseBackupIcon /> },
]

export default function MeterGroupIcons() {
  return <MeterGroup aria-label="Storage by type" values={STORAGE} className="w-full max-w-md" />
}
```

### Label position

`labelPosition="start"` puts the legend above the track; `labelOrientation="vertical"` lists it in a column.

```tsx
import { MeterGroup } from "@booleanpress/ui/meter-group"

const SENDS = [
  { label: "Delivered", value: 72, color: "var(--success)" },
  { label: "Deferred", value: 9, color: "var(--warning-solid)" },
  { label: "Bounced", value: 4, color: "var(--destructive)" },
]

export default function MeterGroupLabelPosition() {
  return (
    <div className="flex w-full max-w-md flex-col gap-10">
      <MeterGroup aria-label="Today's sends, legend first" values={SENDS} labelPosition="start" />
      <MeterGroup aria-label="Today's sends, legend in a column" values={SENDS} labelOrientation="vertical" />
    </div>
  )
}
```

### Vertical

`orientation="vertical"` stands the track up, the legend beside it.

```tsx
import { MeterGroup } from "@booleanpress/ui/meter-group"

const STORAGE = [
  { label: "Attachments", value: 24, color: "var(--chart-5)" },
  { label: "Logs", value: 16, color: "var(--chart-2)" },
  { label: "Templates", value: 24, color: "var(--chart-4)" },
  { label: "Backups", value: 12, color: "var(--chart-3)" },
]

export default function MeterGroupVertical() {
  return <MeterGroup aria-label="Storage by type" orientation="vertical" values={STORAGE} className="h-72" />
}
```

### Min and max

`max={200}` makes each segment's share its value out of 200.

```tsx
import { MeterGroup } from "@booleanpress/ui/meter-group"

// The plan allows 200 GB, so 16 GB of attachments is 8% of the track.
const STORAGE = [
  { label: "Attachments", value: 16, color: "var(--chart-5)" },
  { label: "Logs", value: 8, color: "var(--chart-2)" },
  { label: "Templates", value: 24, color: "var(--chart-4)" },
  { label: "Backups", value: 10, color: "var(--chart-3)" },
]

export default function MeterGroupMinMax() {
  return <MeterGroup aria-label="Storage, of 200 GB" values={STORAGE} max={200} className="w-full max-w-md" />
}
```

### Custom legend

Cards of your own above the track, with `MeterGroupMeters` and `formatValue` writing gigabytes.

```tsx
import { DatabaseBackupIcon, FileTextIcon, LayoutTemplateIcon, PaperclipIcon } from "lucide-react"
import { MeterGroup, MeterGroupMeters } from "@booleanpress/ui/meter-group"
import { useUiLocale } from "@booleanpress/ui/provider"

const STORAGE = [
  { label: "Attachments", value: 50, color: "var(--chart-5)", Icon: PaperclipIcon },
  { label: "Logs", value: 30, color: "var(--chart-2)", Icon: FileTextIcon },
  { label: "Templates", value: 40, color: "var(--chart-4)", Icon: LayoutTemplateIcon },
  { label: "Backups", value: 20, color: "var(--chart-3)", Icon: DatabaseBackupIcon },
]

export default function MeterGroupCustomLegend() {
  const { locale } = useUiLocale()
  const gigabytes = new Intl.NumberFormat(locale, { style: "unit", unit: "gigabyte" })
  const used = STORAGE.reduce((sum, item) => sum + item.value, 0)

  return (
    <MeterGroup aria-label="Storage, of 200 GB" values={STORAGE} max={200} formatValue={(value) => gigabytes.format(value)} className="w-full max-w-sm">
      <ul aria-hidden="true" className="grid grid-cols-2 gap-3.5">
        {STORAGE.map(({ label, value, color, Icon }) => (
          <li key={label} className="flex items-start justify-between gap-2 rounded-xl border bg-card p-4.5 shadow-sm">
            <span className="flex flex-col gap-1">
              <span className="text-muted-foreground">{label}</span>
              <span className="text-lg/7 font-bold">{gigabytes.format(value)}</span>
            </span>
            <Icon className="size-4" style={{ color }} />
          </li>
        ))}
      </ul>
      <div className="flex justify-between text-muted-foreground">
        <span>Storage</span>
        <span>
          {gigabytes.format(used)} / {gigabytes.format(200)}
        </span>
      </div>
      <MeterGroupMeters />
    </MeterGroup>
  )
}
```

## Accessibility

**Semantics.** A group (`role="group"`) of meters: each segment is `role="meter"` with `aria-valuenow`, `aria-valuemin`, `aria-valuemax` and `aria-valuetext`. The legend repeats what the meters say, so it is hidden from assistive technology.

**Labels.** Name the group with `aria-label` or `aria-labelledby` ("Storage by type"). Each meter is named by its segment's `label` and its value is read as `formatValue`'s text, a percentage by default.

**Focus.** A meter group takes no focus and has no keyboard behaviour.

**Known limits.**

- Some older screen readers announce a meter as a progress bar or as plain text with its name and value.
- Colour tells the segments apart in the track; the legend's labels name them. Keep the legend, or a custom one, beside the track.
- A custom legend you render yourself is read as well as the meters: hide it with `aria-hidden` when it only repeats them.

### Keyboard

| Key | Behaviour |
| --- | --- |

## API

### MeterGroup

Renders a `div` and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `values` (required) | `MeterGroupItem[]` |  | The segments, in order along the track. |
| `formatValue` | `((value: number, percent: number) => string)` |  | Writes a segment's value, for the legend and for screen readers. Defaults to its share as a percentage, in the provider's locale. |
| `labelOrientation` | `"horizontal" \| "vertical"` |  | How the built-in legend lists its labels: in a row that wraps, or one under another. Follows `orientation` by default. |
| `labelPosition` | `"start" \| "end"` | `end` | Where the built-in legend goes: after the track (`end`) or before it (`start`). |
| `max` | `number` | `100` | The top of the range: a segment's share of the track is its value over `max - min`. |
| `min` | `number` | `0` | The bottom of the range. |
| `orientation` | `"horizontal" \| "vertical"` | `horizontal` | `horizontal` lays the track across with the legend under it; `vertical` stands it up with the legend beside it. |

### MeterGroupMeters

Renders a `div` and passes it every other prop.

### MeterGroupLegend

Renders a `ol` and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `orientation` | `"horizontal" \| "vertical"` |  | In a row that wraps (`horizontal`) or one under another (`vertical`). Follows the group's orientation by default. |

Every part takes `className`, merged with its defaults by `cn()`, and `ref`, which reaches the element it renders.

**Data attributes:** `data-slot="meter-group"` (MeterGroup), `data-slot="meter-group-meters"` (MeterGroupMeters), `data-slot="meter-group-legend"` (MeterGroupLegend), and `data-orientation`.

## Theming

The track is a 6 px `--border` bar with a 6 px radius; the segments take `--chart-1` to `--chart-5` in turn, or any colour you pass. Legend text is 14 px `--foreground`.

| Token | Used for |
| --- | --- |
| `--border` | background |
