Skip to the content

ComponentsMisc

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"

Usage

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

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.

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.

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.

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.

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.

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.

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.

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

Keyboard
KeyBehaviour

API

MeterGroup

Renders a div and passes it every other prop.

MeterGroup props
PropTypeDefaultDescription
valuesrequiredMeterGroupItem[]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"endWhere the built-in legend goes: after the track (end) or before it (start).
maxnumber100The top of the range: a segment's share of the track is its value over max - min.
minnumber0The bottom of the range.
orientation"horizontal" | "vertical"horizontalhorizontal 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.

MeterGroupLegend props
PropTypeDefaultDescription
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.

The tokens its classes read. Change their values in your theme, and it follows.

Theme tokens
TokenUsed for
--borderbackground