Usage meter

Free · MIT

Labeled usage meter like "SMS credits 7,420 / 10,000" whose fill springs to the new value and shifts to amber and rose near the limit, with optional per-seat segments.

MarkdownOpen in v0
Open in v0

Installation

$ npx shadcn@latest add https://goodui.dev/r/meter.json

The file lands in components/goodui/meter.tsx and is yours to edit. With the @goodui registry added, npx shadcn@latest add @goodui/meter works too.

Usage

import { Meter, type MeterTone } from "@/components/goodui/meter"

function smsHint({ tone, percent }: { tone: MeterTone; percent: number }) {
  if (tone === "danger") return `${percent}% used. Reminders pause at 10,000. Top up to keep them sending.`
  if (tone === "warning") return `${percent}% used. At this pace you run out before Dec 1.`
  return `${percent}% used. Renews Dec 1.`
}

export function PlanUsage({ sms, storageGb, seats }: { sms: number; storageGb: number; seats: number }) {
  return (
    <div className="flex flex-col gap-5 rounded-xl border bg-card p-5">
      <Meter label="SMS credits" value={sms} max={10000} hint={smsHint} />
      <Meter
        label="Photo storage"
        value={storageGb}
        max={20}
        unit=" GB"
        format={{ minimumFractionDigits: 1, maximumFractionDigits: 1 }}
      />
      <Meter label="Team seats" value={seats} max={10} segments={10} size="lg" hint={`${10 - seats} seats left`} />
    </div>
  )
}

When to use

Use it for
  • Plan limits on a billing or settings page: "SMS credits 7,420 / 10,000", storage in GB, API calls.
  • Seat usage, where segments={seats} shows one block per seat.
  • Any measured amount within a known range where getting close to the limit matters.
Reach for something else
  • Progress of a task that finishes (upload, import, onboarding): use progress, which uses role="progressbar".
  • A single percentage in a compact space such as a card corner: use circular-progress.
  • A trend over time rather than a current level: use sparkline-draw.
If you needUse
Plain quotalabel, value, max (defaults warn at 75%, danger at 90%)
Units like GBunit=" GB" and format={{ maximumFractionDigits: 1 }}
One block per seat or slotsegments={max}
A message that changes with the levelhint={({ tone, percent }) => …}
Stricter or looser thresholdswarnAt={0.6} dangerAt={0.85}

Examples

API quota with custom thresholds

<Meter
  label="API requests this month"
  value={84210}
  max={100000}
  warnAt={0.6}
  dangerAt={0.85}
  format={{ notation: "compact" }}
  size="sm"
/>

API reference

PropTypeDefault
label*
Name shown above the bar and used as the meter's accessible name, e.g. "SMS credits".
React.ReactNode—
value*
Amount used. It rolls with NumberFlow and the fill springs to it; it is clamped between 0 and max for the bar.
number—
max*
Limit the value is measured against, e.g. 10000 credits.
number—
format
Formats value and max, e.g. { maximumFractionDigits: 1 }.
Intl.NumberFormatOptions—
locale
Locale passed to Intl.NumberFormat for value and max. Defaults to the GooduiLocaleProvider language, else en-US.
string—
unit
Text after the numbers, e.g. " GB".
string""
warnAt
Share of max (0 to 1) where the bar turns amber.
number0.75
dangerAt
Share of max (0 to 1) where the bar turns rose.
number0.9
segments
Split the bar into this many segments, e.g. one per seat.
number—
hint
Small line under the bar. Receives the current tone and percentage.
React.ReactNode | ((state: { tone: MeterTone; percent: number }) => React.ReactNode)—
size
Bar height: sm 6px, md 8px, lg 12px.
"sm" | "md" | "lg""md"
labels
Text for built-in controls and screen readers; pass translations here.
Partial<MeterLabels>—

Also accepts every <div> attribute.

Accessibility

  • The bar is role="meter" with aria-valuemin 0, aria-valuemax max and aria-valuenow, and is named by the label through aria-labelledby.
  • aria-valuetext reads "7,420 of 10,000, 74%" (with the unit after max). Translate it with labels.valueText(value, max, percent).
  • The visible numbers use number-flow, which keeps one sr-only copy of the value; the " / max" text is aria-hidden because the meter already announces it.
  • Tone is never the only signal: the numbers and percent are always shown, and hint can say it in words.

Motion

  • The fill is translated (not resized) on a spring (stiffness 120, damping 20, mass 0.9). With segments, each segment starts 25 ms after the previous one.
  • The value digits roll through number-flow; the fill and hint colors cross-fade over 500 ms when the tone changes.
  • At 0 the fill fades out (150 ms after a 300 ms delay) so no sliver stays at the track edge.
  • Reduced motion: the fill and digits jump to the new value with duration 0.

Notes for AI

Coding agents read these rules through MCP and llms.txt, or paste this page with Copy page.

  • value and max are raw numbers; pass unit and format instead of building "12.6 GB" yourself.
  • Tone is computed from value / max against warnAt and dangerAt (shares from 0 to 1, not percents). There is no tone prop to force a color.
  • hint can be a function of { tone, percent }; use it for the message instead of reading the tone outside.
  • segments should usually equal max for seats (segments={10} with max={10}); with other values each segment covers max / segments.
  • Values above max are clamped to a full bar but the number still shows the real value.
  • labels={{ valueText: (v, m, p) => `${v} von ${m}, ${p} %` }} translates aria-valuetext; v and m arrive formatted. Under RTL the bar fills from the right.

FAQ

Meter or progress?

Meter for a level within a range that does not "finish" (quota, storage). Progress for work moving toward done. They also use different ARIA roles.

Can I change the colors?

Normal uses bg-primary from your theme; warning is amber and danger is rose, with dark-mode shades. Change the FILL and TEXT maps in the installed file to use other colors.

Related