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.
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 isrole="meter"witharia-valuenow,aria-valuemin,aria-valuemaxandaria-valuetext. The legend repeats what the meters say, so it is hidden from assistive technology. - Labels
- Name the group with
aria-labeloraria-labelledby("Storage by type"). Each meter is named by its segment'slabeland its value is read asformatValue'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-hiddenwhen it only repeats them.
Keyboard
| Key | Behaviour |
|---|
API
MeterGroup
Renders a div and passes it every other prop.
| Prop | Type | Default | Description |
|---|---|---|---|
valuesrequired | 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.
The tokens its classes read. Change their values in your theme, and it follows.
| Token | Used for |
|---|---|
--border | background |