Usage Meter React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React usage meter for plan quotas: tokens, messages, images, credits or money against limits, with resets, warnings and an upgrade offer.
Read it at a glance: each meter shows what is used against its limit, the percent and a bar; the bars grow from empty one after another the first time the card scrolls into view, then glide when the numbers change. At 75% a bar turns amber with an Almost at your limit note, at 90% red, and at the limit it stays full in red and says how far over it went; a word and an icon always say it too, and a screen reader hears each crossing once through a polite status (never on load). Resets read today at 18:00, tomorrow or in 3 days up close and a date further out, with the full date on hover. The upgrade offer slides open once a meter reaches the warning line; its button is a real link when it has an href. The compact variant is a small ring with the percent of the fullest meter for a composer or a topbar: click or tap it, or press Enter or Space on it, and the details open in a popover whose panel takes focus first (Tab goes on to the upgrade button); Escape or a click outside closes it and focus returns to the ring. On a phone the details rise as a bottom sheet with 14px rows and a full width upgrade button pinned at the bottom. Every button has a 44px touch target. In a narrow spot (under 352px) a row puts its value on a line of its own under the label (wider, a value that does not fit wraps instead of breaking the label), and a narrow offer puts its button under the text, so nothing gets squeezed. Press Use 50,000 tokens in the playground to push the token meter over both thresholds. Reduced motion shows the values at once.
Usage Meter is a Pro React component in the React AI components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add usage-meter, together with Progress, Badge, Button, Popover, Skeleton and 10 more and the npm packages motion and lucide-react.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add usage-meterUsage Meter is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import UsageMeter from @/components/ui/usage-meter.
import { Coins, ImageIcon, MessageSquare, Wallet } from "lucide-react"
import { UsageMeter, type UsageMeterItem } from "@/components/ui/usage-meter"
const meters: UsageMeterItem[] = [
{ id: "tokens", label: "Tokens", used: 742300, limit: 1000000, unit: "tokens", icon: <Coins /> },
{
id: "messages",
label: "Northwind Pro messages",
used: 38,
limit: 50,
unit: "messages",
icon: <MessageSquare />,
resetsAt: "2026-09-24T18:00:00-04:00",
},
{ id: "images", label: "Generated images", used: 12, limit: 20, unit: "images", icon: <ImageIcon /> },
{ id: "cost", label: "Cost API", used: 412.8, limit: 500, unit: "currency", icon: <Wallet /> },
]
export function PlanUsage() {
return (
<UsageMeter
meters={meters}
plan={{ name: "Pro" }}
resetsAt="2026-10-01T00:00:00-04:00"
upgrade={{ label: "Upgrade to Business", description: "$59 / month, unlimited tokens", href: "/billing" }}
locale="en-US"
/>
)
}Props
meters*UsageMeterItem[]The meters: { id, label, used, limit?, unit?, currency?, icon?, description?, resetsAt?, format? }. limit null or undefined is unlimited. unit is tokens, messages, credits, requests, images, currency or count (the default): tokens print exact in rows and compact (742,3 K) in rings, currency goes through formatPrice with two decimals, the rest through formatNumber, and the word units add their word after the limit. format replaces the number formatting. An empty list renders nothing.
variantdefault"card""card" | "inline" | "compact"card: a raised panel with the title, the plan badge, the reset date and the upgrade offer, for settings and billing pages. inline: the rows alone (the title is for screen readers), for side panels. compact: a small ring with the percent of the most used meter, for a composer or a topbar; it opens the details in a popover, a bottom sheet on phones.
displaydefault"bar""bar" | "ring"bar: a label, the value and the percent over a bar for each meter. ring: a grid of rings with the percent inside and the label under each, 2 a row on a phone and up to 4 once the list is 448px wide (dashboards).
titledefault""ReactNodeThe card title, also the popover title in compact. Empty falls back to labels.title.
plan{ name: ReactNode; tone?: Tone }The current plan as a soft badge next to the title, in its own tone (neutral by default).
upgrade{ label: ReactNode; description?: ReactNode; href?: string; onClick?: () => void; tone?: Tone }The upgrade offer: a bold line, a muted line and a pill button labelled labels.upgrade. With href the button is a link; in the compact popover picking it closes the popover first. tone gives the offer its own color (brand on neutral meters); without it the offer follows tone and color. No upgrade, no offer.
upselldefault"auto""auto" | "always" | "never"When the upgrade offer shows: auto once any meter reaches the warning threshold (it slides open when that happens), always, or never.
resetsAtDate | string | numberWhen the plan's meters reset, shown in the card header (under the rows in inline, under the popover title in compact). A meter's own resetsAt wins and shows in its caption. A reset in the past is hidden.
resetFormatdefault"auto""auto" | "date" | "relative"auto: today at 18:00 or tomorrow up close, in 3 days within a week, then the date. date: always the date (with the time when it is not midnight). relative: in 1 hour, in 7 days. Before hydration it is always the date, so server and client agree.
thresholdsdefault"{ warning: 75, danger: 90 }"{ warning?: number; danger?: number }Percentages at which a meter turns warning (amber) and danger (red), with a note under it. At or past the limit a meter is always over, full and red. Out of order values are swapped; leave one out to skip that step.
showRemainingdefaultfalsebooleanRows read 257,700 tokens left instead of 742,300 / 1,000,000 tokens (labels.remaining), and rings show what is left under them. A meter past its limit keeps used / limit and says how far over it is.
showPercentdefaulttruebooleanThe percent at the end of each row, inside each ring and next to the compact ring. Below the limit it rounds down, so 99.6% never reads as full.
sizedefault"md""sm" | "md"sm: 4px bars, 13px text and 48px rings, for narrow side panels. md: 6px bars, 14px text and 64px rings. The phone sheet of the compact variant always uses md.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"The fill of meters below the warning threshold, the tint of the upgrade offer and its button. Crossed thresholds always use warning and danger.
colordefault""stringAny CSS color. Replaces the tone for healthy meters, the offer's tint and its button; warning and danger keep their colors.
radiusdefault"md""none" | "sm" | "md" | "lg"Card corners: 0, 12, 16 or 24px. The upgrade offer inside follows a step smaller.
loadingdefaultfalsebooleanSkeleton rows (or rings, or a skeleton trigger) with the real rows' heights, one per meter or 3 when the list is empty, and aria-busy. Data that arrives after loading is never announced as a crossing.
currencydefault"USD"stringISO 4217 code for currency meters without their own currency.
openbooleanCompact: whether the details are open (controlled). Pair it with onOpenChange.
defaultOpendefaultfalsebooleanCompact: the details start open.
onOpenChange(open: boolean) => voidCompact: called when the details open or close.
responsivedefaulttruebooleanCompact: below 768px the details open as a bottom sheet with a pinned, full width upgrade button. Off keeps the floating panel on phones.
sidedefault"bottom""top" | "right" | "bottom" | "left"Compact: the side of the ring the details open on (it flips when there is no room). top suits a composer at the bottom of the screen.
aligndefault"center""start" | "center" | "end"Compact: how the 416px details panel lines up with the ring (never wider than the screen minus 24px). end suits a ring at the right edge.
animateddefaulttruebooleanBars and rings grow from empty the first time they are on screen, one after another, and glide on the smooth spring when the numbers change. Off shows the values at once, as reduced motion does.
localedefault""stringIntl locale for numbers, money, percents and dates. Empty uses UIProvider's locale, then the page's.
nowDate | numberThe reference time for relative resets. Defaults to the current minute (useNow); pin it for stable screenshots and tests.
labelsPartial<UsageMeterLabels>Every string, English by default (DEFAULT_USAGE_METER_LABELS): title "Usage", usedOf (used, limit), remaining (formatted, count), unlimited, resets (when, kind), at (day, time), over (formatted, count), warning "Almost at your limit", danger "Limit nearly reached", reached "Limit reached", upgrade "Upgrade", details "Usage details", valueLabel (used, limit, percent) for screen readers, loading, missing (a dash for a value that is not a number) and units { tokens, messages, credits, requests, images }, each a word or a function of the count for plurals.
classNamesPartial<Record<"header" | "title" | "plan" | "reset" | "list" | "row" | "icon" | "label" | "value" | "percent" | "bar" | "ring" | "footer" | "status" | "description" | "upsell" | "upgrade" | "trigger" | "content", string>>Extra classes for the inner parts; trigger and content are the compact ring button and its details panel.
classNamestringExtra classes for the root, merged last.
Parts
UsageMeterRow
componentOne meter on its own (label, value, percent, bar, status and reset line) for layouts of your own; delay is the ms it waits before growing once on screen. Props: { meter: UsageMeterItem; size?; tone?; color?; thresholds?; showRemaining?; showPercent?; resetFormat?; currency?; locale?; now?; labels?; animated?; delay?: number; classNames? }.
getUsageState
functionWhere a meter stands, for your own badges or alerts: the percent used (not capped, 52 of 50 is 104) and its state. No limit (null, undefined or Infinity) is unlimited; a limit of 0 is over as soon as anything is used; negative use counts as 0; a used value or limit that is not a number (NaN) has no percent and its row shows a dash with no bar. Signature: (meter: { used: number; limit?: number | null }, thresholds?) => { percent: number | null; state: "ok" | "warning" | "danger" | "over" | "unlimited" }.
DEFAULT_USAGE_METER_LABELS
constantThe English strings, to spread and override. Type: UsageMeterLabels.
DEFAULT_USAGE_METER_THRESHOLDS
constantThe default thresholds, for getUsageState and your own checks. Type: { warning: 75; danger: 90 }.
Examples
Compact in a composer
variant compact: a ring and the percent of the fullest meter; the details open above it, a sheet on phones.
Rings
display ring for a dashboard: 2 a row on a phone, 4 once there is room.
Over the limit
52 of 50 messages: the bar stays full in red and says how far over. upgrade.tone brand colors only the offer.
Unlimited and remaining
An unlimited meter shows what was used; showRemaining turns the rest into what is left.
Money
unit currency with two decimals; each meter can bring its own currency.
Loading, small, in a side panel
variant inline at size sm inside a Card; the skeleton keeps the rows' heights. Refresh to see it again.
English defaults
No labels passed: English copy, singular and plural units, en-US numbers and dates.
Dependencies
Files
- components/ui/usage-meter.tsx
FAQ
Is the React Usage Meter component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Usage Meter?
Run npx wingo-ui@latest add usage-meter. It also adds the Wingo UI items it builds on (Progress, Badge, Button, Popover, Skeleton and 10 more) and the npm packages motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/usage-meter.
How does Usage Meter behave on phones?
Compact: below 768px the details open as a bottom sheet with a pinned, full width upgrade button. Off keeps the floating panel on phones. The responsive prop controls this and is on by default.