Usage meter
Free · MITLabeled 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.
Installation
$ npx shadcn@latest add https://goodui.dev/r/meter.jsonThe 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
- 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.
- 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 need | Use |
|---|---|
| Plain quota | label, value, max (defaults warn at 75%, danger at 90%) |
| Units like GB | unit=" GB" and format={{ maximumFractionDigits: 1 }} |
| One block per seat or slot | segments={max} |
| A message that changes with the level | hint={({ tone, percent }) => …} |
| Stricter or looser thresholds | warnAt={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
| Prop | Type | Default |
|---|---|---|
| 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. | number | 0.75 |
| dangerAt Share of max (0 to 1) where the bar turns rose. | number | 0.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
Linear progress in four variants: spring-filled determinate, indeterminate double sweep, moving stripes and segmented steps.
SVG ring or 270° gauge whose arc springs to the value via stroke-dashoffset, with a rolling percentage or custom label in the center.
Digits roll to the new value with locale formatting, prefixes and suffixes.
KPI card with seven variants, tones, shadows and sizes: count-up or rolling NumberFlow values, auto-toned delta badges (invertible for no-show rates), line, area or bar sparklines that draw in, icons, footers and a skeleton state.