DocumentationComponentsTheme CreatorGitHub
Theme CreatorGitHubIntroduction
InstallationUsageTheming
ComponentsAccordionAction SheetAlertAlertDialogArea ChartAspectRatioAvatarBadgeBannerBar ChartBottom NavBreadcrumbButtonButtonGroupCalendarCardCarouselChartChatBubbleChatBubbleNewCheckboxChipCoachMarkCodeBlockCollapsibleColor PickerComboboxCommandContainerContextMenuDate PickerDividerDrawerDropdown MenuEmptyStateFABFieldFieldsetFile UploadFlexFormGridHoverCardIconButtonLabelLine ChartLinkMenubarMeterModalNavigation MenuNumberFieldOTP InputPaginationPickerPie ChartPopoverProgressPromptInputRadar ChartRadial ChartRadioRatingScroll AreaSearchSegmentedControlSelectShortcutSidebarSkeletonSliderCircularSliderMediaTrimmerSpacerSpinnerSplit ButtonStackStatusStepperSurfaceSwitchTableTabsTextareaTime PickerToastToggleButtonToggleGroupTokenizerToolbarTooltipTop Header DesktopTop Header Mobile
Contributing
Components

Meter

Gauge displaying a known value within a fixed range — battery, disk usage, score, capacity. Linear or circular, four sizes, full intent palette with optional threshold-based auto-coloring, locale-aware formatting, and a multi-segment MeterGroup variant for stacked usage.

Playground

Installation

pnpm add @tessinaui/ui

Usage

import { Meter, MeterGroup } from "@tessinaui/ui";
{/* Basic */}
<Meter value={68} label="Storage" />

{/* Circular */}
<Meter type="circular" value={72} label="CPU" valuePosition="inside" />

{/* Threshold-based auto colour */}
<Meter
  value={92}
  label="Disk usage"
  thresholds={[
    { at: 50, intent: "none" },
    { at: 80, intent: "warning" },
    { at: 101, intent: "error" },
  ]}
/>

{/* Locale-aware formatting */}
<Meter
  value={186.4}
  max={500}
  label="Disk"
  format={{ style: "unit", unit: "gigabyte", maximumFractionDigits: 1 }}
/>

{/* Custom range — rating */}
<Meter value={4.3} max={5} label="Rating" intent="success" showFraction />

{/* Multi-segment */}
<MeterGroup
  max={500}
  showTotal
  format={{ style: "unit", unit: "gigabyte", maximumFractionDigits: 1 }}
  segments={[
    { label: "Photos", value: 124,  intent: "success" },
    { label: "Videos", value: 86.5, intent: "error"   },
    { label: "Music",  value: 42.3, intent: "info"    },
    { label: "Other",  value: 31.2, intent: "none"    },
  ]}
/>

Examples

Default

A basic linear meter with a label and right-aligned value.

Sizes

Five track heights — xs, sm, md, lg, and xl. xs is the hairline rule that reads as part of a dense row rather than as a bar of its own.

Intents

The house intent vocabulary: none (the neutral default), info, success, warning, error.

Thresholds

The indicator recolors itself as the value crosses breakpoints — error, then warning, then success.

Circular

The ring variant with the value centered inside via type="circular" and valuePosition="inside".

Multi-segment

A MeterGroup stacks several colored segments into one bar with a legend and total.

Storage breakdown

showRemaining names the unfilled part of the track. "How much is left" is the number a storage or seat meter actually gets read for, and it is the one a stacked bar cannot show on its own.

Status and icons

icon puts a glyph in the label row. The status glyph is automatic whenever thresholds are in play (or the intent is error/warning) and the meter shows a label, so the state is never carried by fill colour alone — see Contrast below. It captions the label, so a label-less meter shows colour and value text only; pass statusIcon to force it, or statusIcon={false} to opt out.

Target marks

marks places static reference ticks on the track — a goal, a budget, a limit.

Scale labels

minLabel and maxLabel name the ends of the range. Apple's guidance is to describe the current value and both endpoints; a screen reader reads the visible labels.

Segmented

segments renders the track as N discrete cells that fill completely, never partially. This is the shape a battery, a signal strength or a password meter wants — distinct from MeterGroup, which divides one bar proportionally.

Password strength

The canonical segmented meter: a 0–4 score (the range zxcvbn returns) across four cells, with the strength stated in text rather than by colour alone.

Quota and over-capacity

A value above max clamps the bar — aria-valuenow must stay inside the range — but the text stays truthful and the root carries data-exceeded.

Caption row

description is the helper line under the track. With valuePosition="below" it shares that row: helper at the start, value at the end.

Value positions

right (default), below, inside and none. inside needs a track tall enough to hold text, so at xs–md it falls back to right rather than dropping the value.

Formatting

format and locale take Intl.NumberFormat options. Whatever the meter shows, it announces the same thing — see the accessibility note below.

Comparison list

Several meters in a row with aligned label and value columns — the most common shape a meter appears in.

In a table

xs plus labelPosition="none" gives a dense in-cell meter. The row already names the thing, so the meter takes its accessible name from label without drawing it.

Empty and unknown

Zero renders an empty track; a tiny nonzero value still paints. A quantity with no meaningful maximum is not a meter at all — ARIA says so — so show the number instead.

Loading

Meter vs Progress

The two components look similar but communicate different things:

MeterProgress
PurposeA known value within a fixed rangeTask completion working toward 100%
ExamplesBattery 78%, Disk 320 GB / 512 GB, Score 4.2 / 5, RAM 6.2 GB / 16 GBFile upload, multi-step form, page load
RangeCustom min / max (default 0–100)Always 0–100
IndeterminateNo — value must be knownYes — indeterminate prop
ARIA rolerole="meter"role="progressbar"

Use Meter when the value already exists and isn't going to change as the user waits. Use Progress when something is loading.

API Reference — Meter

PropTypeDefaultDescription
valuenumber—The current value (clamped between min and max)
minnumber0Minimum
maxnumber100Maximum
type"linear" | "circular""linear"Bar or ring
size"xs" | "sm" | "md" | "lg" | "xl""md"Size token
rounded"none" | "sm" | "md" | "lg" | "full""full"Corner radius (linear only — a ring has no corners)
intent"none" | "error" | "warning" | "success" | "info""none"Semantic meaning (§1). none renders the neutral primary fill
variant"solid" | "soft""solid"Visual weight — soft uses the *-light token
thresholdsArray<{ at: number; intent: MeterIntent }>—Auto-recolor based on value crossing breakpoints
labelReactNode—Visible label rendered above (linear) or below (circular) the meter
labelPosition"outside" | "none""outside"Whether the label is drawn. It still names the meter when hidden
valuePosition"right" | "below" | "inside" | "none""right"Where the formatted value renders. inside falls back to right below lg
showFractionbooleanfalseRender as ${value}/${max} instead of ${value}
formatIntl.NumberFormatOptionspercent of rangeLocale-aware formatting (currency, units, compact). Without it the value reads as its percent of the min–max range — 712 on 300–900 is 69%
localeIntl.LocalesArgumentruntimeBCP-47 locale tag
renderValue(formatted: string, value: number) => ReactNode—Fully custom value rendering — overrides format / showFraction
getAriaValueText(formatted: string, value: number) => string—What assistive tech announces. Required alongside renderValue
descriptionReactNode—Helper line under the track. Carbon: optional, except on an error state
iconReactNode—Leading glyph in the label row
statusIconReactNode | booleanautoStatus glyph, rendered beside the label. Defaults to the resolved intent's glyph when thresholds are in play or the intent is error/warning and a label is visible; true forces it without one, false opts out
minLabel / maxLabelReactNode—Scale endpoint labels under the track
marksArray<{ at: number; label?: ReactNode }>—Static reference ticks — a goal, a limit, a target
segmentsnumber—Render the track as N discrete cells that fill completely (linear only)
childrenReactNode—Custom content centred in the ring (type="circular")
dir"ltr" | "rtl"inheritedText direction
classNamestring—Additional classes on the root

Deprecated, one minor. intent="primary" and intent="neutral" both render as none (they were measured identical in dark and near-identical in light), and tone is the old name for variant — §1 reserves tone for default | on-color. Both still work and dev-warn once.

Thresholds

thresholds lets the meter recolour itself based on the current value. The lowest at whose value is greater than the current value wins. If no threshold matches, the base intent is used. at is on the meter's own min–max scale, like marks — a 90% threshold on a 0–10 meter is at: 9.

<Meter
  value={92}
  thresholds={[
    { at: 30, intent: "error" },     // 0–29  → error
    { at: 70, intent: "warning" },   // 30–69 → warning
    { at: 101, intent: "success" },  // 70–100 → success
  ]}
/>

For an inverted scale (e.g. battery — low is bad), reverse the order:

<Meter
  value={15}
  thresholds={[
    { at: 20, intent: "error" },
    { at: 50, intent: "warning" },
    { at: 101, intent: "success" },
  ]}
/>

API Reference — MeterGroup

PropTypeDefaultDescription
segmentsArray<MeterGroupSegment>—Stacked segments that fill the bar
minnumber0Minimum value of the scale
maxnumber | "sum"100Max value, or "sum" to derive from segment values
size"xs" | "sm" | "md" | "lg" | "xl""md"Size token
rounded"none" | "sm" | "md" | "lg" | "full""full"Track corner radius
labelReactNode—Label rendered above the bar — also the accessible name
legendPosition"above" | "below" | "none""below"Where to render the segment legend
showTotalbooleanfalseRender ${total} / ${max} next to the label
showRemainingbooleanfalseAdd a "remaining" entry to the legend for the unfilled track. Ignored when max="sum"
remainingLabelReactNode"Remaining"Label for that entry
formatIntl.NumberFormatOptions—Format the total + each legend value
localeIntl.LocalesArgumentruntimeBCP-47 locale tag
dir"ltr" | "rtl"inheritedText direction

MeterGroupSegment

FieldTypeDescription
valuenumberSegment value (in same scale as min/max)
labelReactNodeDisplay label in the legend
intentMeterIntentColour for this segment (default "none")
variant"solid" | "soft"Visual weight (default "solid")
tone"solid" | "soft"Saturation (default "solid")
iconReactNodeOptional icon next to the legend label

Notes

  • Accessibility — built on @base-ui/react/meter. The container renders with role="meter" and aria-valuemin / aria-valuemax / aria-valuenow / aria-valuetext so assistive technologies announce the value correctly.
  • Locale-aware aria-valuetext — aria-valuetext carries the same Intl.NumberFormat text the meter shows: the percent of the min–max range by default, or the value in your format.
  • Every descendant of a meter is presentational. ARIA forces role="presentation" on everything inside role="meter", so the visible value is not announced on its own — it only reaches assistive tech through aria-valuetext. showFraction and format handle this for you; if you pass renderValue, pass getAriaValueText with it or the screen and the screen reader will disagree.
  • A meter is not focusable and carries no keyboard interaction, no aria-live and no interactive children — a control inside a meter is unreachable.
  • Colour is never the only signal. When thresholds are in play and a label is visible, the status glyph renders automatically, because several intent fills do not clear the 3:1 non-text contrast floor against the track — see Contrast. A label-less meter leans on its value text instead, so keep that visible.
  • Inside value — only lg and xl linear sizes render an inline value; below that it falls back to right rather than dropping the value.
  • RTL — the linear track is mirrored via transform: scaleX(-1). The inside-value text is un-mirrored so it reads correctly.
  • max="sum" on MeterGroup — convenient when you want the bar to be exactly as long as the data fills it (e.g. a triage panel showing exact bug counts), rather than a fixed scale.
  • segments means two different things, deliberately: on Meter it is a number of discrete cells (the Progress-family meaning), and on MeterGroup it is the array of proportional parts.
  • We render div[role="meter"], not the native <meter> element. MDN's stated best practice is the native element, but it is effectively unstyleable across engines (::-webkit-meter-* only) — which is why Base UI, React Aria and Spectrum all make the same deviation.

Contrast

Meter's fill inherits the shared intent tokens and its track is bg-secondary, and several of those pairs do not clear the 3:1 that WCAG 1.4.11 asks of a meaningful graphic. Measured on the live component, both themes:

FillLightDark
none (primary)14.2:114.5:1
info4.1:15.7:1
error3.8:12.8:1
success2.6:16.8:1
warning1.7:18.9:1
variant="soft", any intent1.2:11.0:1

Light-theme amber cannot reach 3:1 against the track even at full saturation, so this is not a tuning miss — it is the same physics the design system already records for the amber presence dot. Two consequences for consumers:

  1. Never let the fill be the only carrier. Keep the value text visible, or keep the automatic status glyph. Both are on by default — but the glyph needs a visible label, so a label-less meter must keep its value text (or set statusIcon).
  2. variant="soft" is a de-emphasis treatment, not a way to show a value. Use it where the number beside it does the talking.

Retuning the shared tokens is a system-level decision, tracked separately.

MenubarModal

On this page

PlaygroundInstallationUsageExamplesDefaultSizesIntentsThresholdsCircularMulti-segmentStorage breakdownStatus and iconsTarget marksScale labelsSegmentedPassword strengthQuota and over-capacityCaption rowValue positionsFormattingComparison listIn a tableEmpty and unknownLoadingMeter vs ProgressAPI Reference — MeterThresholdsAPI Reference — MeterGroupMeterGroupSegmentNotesContrast