# Meter

> Show a known measurement within a fixed range, such as storage or a score.

Source: https://www.honestui.com/docs/components/meter

```tsx
import {
  Meter,
  MeterIndicator,
  MeterLabel,
  MeterTrack,
  MeterValue,
} from "@/components/honest-ui/ui/meter";

export function MeterDemo() {
  return (
    <div className="flex w-full max-w-sm items-center gap-8">
      <Meter className="min-w-0 flex-1" value={75}>
        <div className="flex items-center justify-between gap-2">
          <MeterLabel>Storage usage</MeterLabel>
          <MeterValue />
        </div>
        <MeterTrack>
          <MeterIndicator />
        </MeterTrack>
      </Meter>
      <Meter aria-label="Storage used" value={75} variant="circular" />
    </div>
  );
}

```

## Overview [#overview]

Use Meter to display a known numeric value within a known range: storage used against quota, budget spent against budget, signal strength, password strength, capacity, a score. The value is a measurement of the current state, not a task advancing toward an end. For work in motion — uploads, imports, installs — use [Progress](/docs/components/progress) instead.

## Anatomy [#anatomy]

A meter has a root carrying the value semantics, an optional label naming what is measured, an optional formatted value, and a visual bar or ring. With no children, `Meter` renders the matching track for its variant plus a centered value automatically.

## Progress vs Meter [#progress-vs-meter]

The two components look alike and differ in meaning, and that difference is exposed to assistive technology through their roles: `Meter` emits `role="meter"` while `Progress` emits `role="progressbar"`. A screen-reader user hearing "progressbar" expects a value that grows and a moment when it finishes; "meter" promises a reading that can go either way and never completes.

Ask one question: does this number end? An import at 72% will hit 100% — Progress. Storage sitting at 72% of quota is just storage — Meter. Quota consumption does creep upward over weeks, but it is not a *task*, there is no completion event, and nobody benefits from a component that implies one.

## Usage guidance [#usage-guidance]

Always give the meter a visible label and, when precision matters, render `MeterValue` or format the announced text yourself — "14.2 GB of 20 GB used" carries more meaning than a bar and "72". When different portions of the range have different meanings (low, normal, high), change the indicator color per band but pair it with wording such as "Weak signal" or "Strong signal"; color alone excludes too many people.

## Accessibility [#accessibility]

The root emits `role="meter"` with `aria-valuemin`, `aria-valuemax`, and `aria-valuenow`. Screen readers announce the accessible name plus the current value against the range. Connect `MeterLabel` for the name, and use `format`, `locale`, or `getAriaValueText(formattedValue, value)` when the default percentage is less informative than real units. Render the value as text for sighted users too — bar length is a poor instrument for comparing 68% against 74%.

Meters have no indeterminate or loading state by design: a measurement is either known or it isn't. While the underlying value is being fetched, show a [Skeleton](/docs/components/skeleton) rather than a zeroed meter, which would falsely read as empty. Value changes animate with a short transition that is skipped under reduced-motion preferences. Colors come from theme tokens and adapt to dark mode automatically.

## Installation [#installation]


  

  
    <CliBlock commands="[&#x22;meter&#x22;]" />
  

  
    
      
        Install the following dependencies:
      

      ```bash
      npm install @base-ui-components/react
      ```

      
        Copy and paste the following code into your project.
      

      ### components/ui/meter.tsx

```tsx
"use client"

import * as React from "react"
import { Meter as MeterPrimitive } from "@base-ui-components/react/meter"

import { cn } from "@/lib/utils"

function Meter({
  className,
  children,
  max = 100,
  min = 0,
  style,
  value,
  variant = "linear",
  ...props
}: MeterPrimitive.Root.Props & {
  variant?: "linear" | "circular"
}) {
  const percentage =
    max > min ? Math.min(100, Math.max(0, ((value - min) / (max - min)) * 100)) : 0

  return (
    <MeterPrimitive.Root
      data-slot="meter"
      data-variant={variant}
      className={cn(
        "group/meter flex w-full flex-col gap-[var(--hui-space-3)]",
        variant === "circular" &&
          "relative items-center justify-center",
        className
      )}
      max={max}
      min={min}
      style={(state) =>
        ({
          ...(typeof style === "function" ? style(state) : style),
          "--hui-meter-percentage": percentage,
        }) as React.CSSProperties & { "--hui-meter-percentage": number }
      }
      value={value}
      {...props}
    >
      {children ? (
        children
      ) : variant === "circular" ? (
        <>
          <MeterCircularTrack />
          <MeterValue />
        </>
      ) : (
        <MeterTrack>
          <MeterIndicator />
        </MeterTrack>
      )}
    </MeterPrimitive.Root>
  )
}

function MeterLabel({ className, ...props }: MeterPrimitive.Label.Props) {
  return (
    <MeterPrimitive.Label
      data-slot="meter-label"
      className={cn(
        "text-[var(--hui-color-foreground-base-primary)] [font-family:var(--hui-font-body)] [font-size:var(--hui-font-size-mini)] [font-weight:var(--hui-font-weight-medium)] [letter-spacing:var(--hui-letter-spacing-mini)] [line-height:var(--hui-line-height-mini)]",
        className
      )}
      {...props}
    />
  )
}

function MeterTrack({ className, ...props }: MeterPrimitive.Track.Props) {
  return (
    <MeterPrimitive.Track
      data-slot="meter-track"
      className={cn(
        "relative block h-[var(--hui-space-2)] w-full overflow-clip rounded-[1px] bg-[var(--hui-color-background-neutral-secondary)]",
        className
      )}
      {...props}
    />
  )
}

function MeterIndicator({
  className,
  style,
  ...props
}: MeterPrimitive.Indicator.Props) {
  return (
    <MeterPrimitive.Indicator
      data-slot="meter-indicator"
      className={cn(
        "h-full origin-left bg-[var(--hui-color-background-accent-emphasis)] [transform:scaleX(calc(var(--hui-meter-percentage,0)/100))] motion-safe:[transition:transform_var(--hui-duration-moderate)_linear]",
        className
      )}
      style={(state) => ({
        ...(typeof style === "function" ? style(state) : style),
        width: "100%",
      })}
      {...props}
    />
  )
}

function MeterValue({ className, ...props }: MeterPrimitive.Value.Props) {
  return (
    <MeterPrimitive.Value
      data-slot="meter-value"
      className={cn(
        "text-right text-[var(--hui-color-foreground-base-primary)] tabular-nums [font-family:var(--hui-font-body)] [font-size:var(--hui-font-size-mini)] [font-weight:var(--hui-font-weight-regular)] [letter-spacing:var(--hui-letter-spacing-mini)] [line-height:var(--hui-line-height-mini)] group-data-[variant=circular]/meter:absolute group-data-[variant=circular]/meter:top-1/2 group-data-[variant=circular]/meter:left-1/2 group-data-[variant=circular]/meter:-translate-x-1/2 group-data-[variant=circular]/meter:-translate-y-1/2 group-data-[variant=circular]/meter:whitespace-nowrap group-data-[variant=circular]/meter:text-center group-data-[variant=circular]/meter:[font-weight:var(--hui-font-weight-medium)]",
        className
      )}
      {...props}
    />
  )
}

function MeterCircularTrack({
  className,
  ...props
}: React.ComponentProps<"svg">) {
  return (
    <svg
      aria-hidden="true"
      data-slot="meter-circular-track"
      viewBox="0 0 72 72"
      className={cn(
        "aspect-square h-[var(--hui-meter-size)] w-[var(--hui-meter-size)] -rotate-90 [--hui-meter-size:var(--hui-space-14)] [--hui-meter-track-size:var(--hui-space-2)]",
        className
      )}
      {...props}
    >
      <circle
        cx="36"
        cy="36"
        data-slot="meter-circular-track-circle"
        r="32"
        vectorEffect="non-scaling-stroke"
        className="fill-none stroke-[var(--hui-color-background-neutral-secondary)] [stroke-width:var(--hui-meter-track-size)]"
      />
      <circle
        cx="36"
        cy="36"
        data-slot="meter-circular-indicator-circle"
        pathLength="100"
        r="32"
        strokeDasharray="100"
        vectorEffect="non-scaling-stroke"
        className="fill-none stroke-[var(--hui-color-background-accent-emphasis)] [stroke-dashoffset:calc(100-var(--hui-meter-percentage,0))] [stroke-linecap:butt] [stroke-width:var(--hui-meter-track-size)] motion-safe:[transition:stroke-dashoffset_var(--hui-duration-moderate)_linear]"
      />
    </svg>
  )
}

export {
  Meter,
  MeterLabel,
  MeterTrack,
  MeterIndicator,
  MeterValue,
  MeterCircularTrack,
}

```

      
        Update the import paths to match your project setup.
      
    
  


## Usage [#usage]

```tsx
import {
  Meter,
  MeterCircularTrack,
  MeterIndicator,
  MeterLabel,
  MeterTrack,
  MeterValue,
} from "@/components/ui/meter";
```

```tsx
<Meter value={40}>
  <div className="flex items-center justify-between">
    <MeterLabel>Progress</MeterLabel>
    <MeterValue />
  </div>
  <MeterTrack>
    <MeterIndicator />
  </MeterTrack>
</Meter>
```

```tsx
<Meter value={40} variant="circular">
  <MeterCircularTrack />
  <MeterValue />
</Meter>
```

Non-zero minimums work: `<Meter min={20} max={80} value={65} />` maps 65 into the correct fraction of the bar.

## Don't do this [#dont-do-this]

### A task pretending to be a measurement [#a-task-pretending-to-be-a-measurement]

```tsx
// Bad
<Meter value={uploadedBytes / totalBytes * 100}>
  <MeterLabel>Uploading photos</MeterLabel>
</Meter>
```

```tsx
// Good
<Progress value={uploadedBytes / totalBytes * 100}>
  <ProgressLabel>Uploading photos</ProgressLabel>
</Progress>
```

`role="meter"` tells assistive technology this is a static reading, so users will not wait for completion feedback that never arrives — and the UI has no natural way to say "done". Anything with an endpoint belongs to Progress.

### Thresholds conveyed by color only [#thresholds-conveyed-by-color-only]

```tsx
// Bad
<Meter value={95} className="[&_[data-slot=meter-indicator]]:bg-red-500">
  <MeterLabel>CPU</MeterLabel>
</Meter>
```

```tsx
// Good
<div>
  <p className="text-sm font-medium">CPU — critical load</p>
  <Meter value={95}>...</Meter>
</div>
```

Red at 95% is invisible to colorblind users, washed out in forced-colors mode, and silent to screen readers. Keep the color if you like it, but state the verdict in words where everyone can get it.

### A bare number without units [#a-bare-number-without-units]

```tsx
// Bad
<Meter value={72} />
```

```tsx
// Good
<Meter value={72} format={{ style: "unit", unit: "percent" }}>
  <MeterLabel>Storage used</MeterLabel>
  <MeterValue />
</Meter>
```

An unlabeled bar with no value text answers no question: 72 of what, out of how much, measured when? Name the measurement and show its units; use `format` or `getAriaValueText` so the announcement matches what sighted users see.

## Examples [#examples]

### Storage quota [#storage-quota]

Real units on both sides of the comparison.

```tsx
import { Meter, MeterIndicator, MeterTrack, MeterValue } from "@/components/honest-ui/ui/meter"

export function MeterStorageQuota() {
  return (
    <Meter value={72} className="w-full max-w-xs">
      <div className="flex justify-between text-sm font-medium">
        <span>Storage used</span>
        <MeterValue />
      </div>
      <MeterTrack className="rounded-full"><MeterIndicator /></MeterTrack>
    </Meter>
  )
}

```

### Without Label and Value [#without-label-and-value]

The automatic layout with only the root's children generated.

```tsx
import { Meter } from "@/components/honest-ui/ui/meter"

export function MeterSimpleDemo() {
  return <Meter className="w-full max-w-64" value={50} />
}

```

### With Formatted Value [#with-formatted-value]

```tsx
"use client"

import {
  Meter,
  MeterIndicator,
  MeterLabel,
  MeterTrack,
  MeterValue,
} from "@/components/honest-ui/ui/meter"

export function MeterWithFormattedValueDemo() {
  return (
    <Meter className="w-full max-w-64" value={3} max={5}>
      <div className="flex items-center justify-between gap-2">
        <MeterLabel>Rating</MeterLabel>
        <MeterValue>{(_formatted, value) => `${value} / 5`}</MeterValue>
      </div>
      <MeterTrack>
        <MeterIndicator />
      </MeterTrack>
    </Meter>
  )
}

```

### With Range [#with-range]

A non-zero minimum and maximum mapping values into a partial band.

```tsx
"use client"

import {
  Meter,
  MeterIndicator,
  MeterLabel,
  MeterTrack,
  MeterValue,
} from "@/components/honest-ui/ui/meter"

export function MeterWithRangeDemo() {
  return (
    <Meter className="w-full max-w-64" value={700} min={500} max={1000}>
      <div className="flex items-center justify-between gap-2">
        <MeterLabel>Bandwidth (Mbps)</MeterLabel>
        <MeterValue>{(_formatted, value) => value}</MeterValue>
      </div>
      <MeterTrack>
        <MeterIndicator />
      </MeterTrack>
    </Meter>
  )
}

```

### Circular Sizes [#circular-sizes]

Set `--hui-meter-size` and `--hui-meter-track-size` on `MeterCircularTrack` to change the rendered diameter and stroke without editing its SVG geometry. Keep the centered value readable as the ring gets smaller.

```tsx
import {
  Meter,
  MeterCircularTrack,
  MeterLabel,
  MeterValue,
} from "@/components/honest-ui/ui/meter"

const sizes = [
  {
    label: "48 px",
    trackClassName:
      "[--hui-meter-size:var(--hui-space-11)] [--hui-meter-track-size:var(--hui-space-1)]",
    valueClassName: "[font-size:var(--hui-font-size-mini)]",
  },
  {
    label: "72 px",
    trackClassName: "",
    valueClassName: "",
  },
  {
    label: "120 px",
    trackClassName:
      "[--hui-meter-size:var(--hui-space-17)] [--hui-meter-track-size:var(--hui-space-3)]",
    valueClassName: "[font-size:var(--hui-font-size-regular)]",
  },
] as const

export function MeterCircularSizesDemo() {
  return (
    <div className="flex flex-wrap items-end justify-center gap-[var(--hui-space-8)]">
      {sizes.map(({ label, trackClassName, valueClassName }) => (
        <Meter
          key={label}
          className="w-auto gap-[var(--hui-space-3)]"
          value={68}
          variant="circular"
        >
          <div className="relative">
            <MeterCircularTrack className={trackClassName} />
            <MeterValue className={valueClassName} />
          </div>
          <MeterLabel>Storage · {label}</MeterLabel>
        </Meter>
      ))}
    </div>
  )
}

```

### Circular Semantic States [#circular-semantic-states]

You can change the circular indicator stroke with a semantic color token. Pair that color with a visible label such as “Weak signal” or “Strong signal” so the state does not depend on color alone.

```tsx
"use client"

import {
  Meter,
  MeterCircularTrack,
  MeterLabel,
  MeterValue,
} from "@/components/honest-ui/ui/meter"

const readings = [
  {
    label: "Weak signal",
    value: 24,
    indicatorClassName:
      "[&_[data-slot=meter-circular-indicator-circle]]:stroke-[var(--hui-color-background-danger-emphasis)]",
  },
  {
    label: "Fair signal",
    value: 58,
    indicatorClassName:
      "[&_[data-slot=meter-circular-indicator-circle]]:stroke-[var(--hui-color-background-attention-emphasis)]",
  },
  {
    label: "Strong signal",
    value: 88,
    indicatorClassName:
      "[&_[data-slot=meter-circular-indicator-circle]]:stroke-[var(--hui-color-background-success-emphasis)]",
  },
] as const

export function MeterCircularStatusesDemo() {
  return (
    <div className="flex flex-wrap justify-center gap-[var(--hui-space-9)]">
      {readings.map(({ label, value, indicatorClassName }) => (
        <Meter
          key={label}
          className="w-auto gap-[var(--hui-space-3)]"
          value={value}
          variant="circular"
        >
          <div className="relative">
            <MeterCircularTrack className={indicatorClassName} />
            <MeterValue>{(_formattedValue, currentValue) => `${currentValue}%`}</MeterValue>
          </div>
          <MeterLabel>{label}</MeterLabel>
        </Meter>
      ))}
    </div>
  )
}

```

## API reference [#api-reference]

`Meter` accepts all Base UI Meter root props plus Honest UI's `variant`:

| Prop          | Values               | Default     |
| ------------- | -------------------- | ----------- |
| `variant`     | `linear`, `circular` | `linear`    |
| `value`       | number (required)    | —           |
| `min` / `max` | number               | `0` / `100` |

Unlike Progress, `value` must be a number — there is no indeterminate state. The root carries `role="meter"` with `aria-valuemin`, `aria-valuemax`, `aria-valuenow`, and `aria-valuetext`; `format` (Intl options), `locale`, and `getAriaValueText(formattedValue, value)` control the announced text. Computed percentages are clamped to 0–100.

Parts: `MeterLabel`, `MeterTrack`, `MeterIndicator`, and `MeterValue` forward their Base UI props; `MeterCircularTrack` accepts native SVG props plus the `--hui-meter-size` and `--hui-meter-track-size` CSS variables.

See the [Base UI Meter API](https://base-ui.com/react/components/meter#api-reference) for value formatting and state details.
