Usage Meters
Billing-period usage bars that turn amber at 80% and red over the limit, with an upgrade prompt only when it matters.
npx shadcn@latest add https://sundaeui.dev/r/usage-meters.json'use client'
export interface Meter {
id: string
label: string
used: number
limit: number
unit?: string
/** Format numbers for display. */
format?: (n: number) => string
}
export interface UsageMetersProps {
meters: Meter[]
/** ISO date when usage resets. */
resetsAt?: string
planName?: string
onUpgrade?: () => void
}
const tone = (r: number) => (r >= 1 ? 'red' : r >= 0.8 ? 'amber' : 'zinc')
/** Billing-period usage with threshold colours, projected overage and an upgrade prompt that appears only when it matters. */
export function UsageMeters({ meters, resetsAt, planName = 'Current plan', onUpgrade }: UsageMetersProps) {
const fmt = (m: Meter, n: number) => (m.format ? m.format(n) : new Intl.NumberFormat('en-US', { notation: n >= 10000 ? 'compact' : 'standard', maximumFractionDigits: 1 }).format(n))
const worst = Math.max(...meters.map((m) => m.used / m.limit))
const days = resetsAt ? Math.max(0, Math.ceil((new Date(resetsAt).getTime() - Date.now()) / 864e5)) : null
return (
<section aria-labelledby="um-h" className="w-full max-w-md rounded-2xl border border-zinc-200 bg-white p-5 text-zinc-900 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-100">
<header className="flex items-baseline justify-between">
<h3 id="um-h" className="text-[15px] font-semibold">
Usage
</h3>
<span className="text-[12.5px] text-zinc-500" suppressHydrationWarning>
{planName}
{days !== null && ` · resets in ${days} ${days === 1 ? 'day' : 'days'}`}
</span>
</header>
<ul className="mt-5 space-y-5">
{meters.map((m) => {
const r = m.used / m.limit
const t = tone(r)
return (
<li key={m.id}>
<div className="flex items-baseline justify-between text-[13.5px]">
<span id={`um-${m.id}`}>{m.label}</span>
<span className="tabular-nums text-zinc-500">
<span className={t === 'red' ? 'font-semibold text-red-600 dark:text-red-400' : t === 'amber' ? 'font-semibold text-amber-600 dark:text-amber-400' : 'text-zinc-900 dark:text-zinc-100'}>{fmt(m, m.used)}</span> / {fmt(m, m.limit)}
{m.unit && ` ${m.unit}`}
</span>
</div>
<div role="meter" aria-labelledby={`um-${m.id}`} aria-valuemin={0} aria-valuemax={m.limit} aria-valuenow={m.used} aria-valuetext={`${Math.round(r * 100)}% used`} className="relative mt-2 h-2 overflow-hidden rounded-full bg-zinc-100 dark:bg-zinc-900">
<span
className={`absolute inset-y-0 left-0 rounded-full transition-[width] duration-700 ease-[cubic-bezier(.22,1,.36,1)] ${t === 'red' ? 'bg-red-500' : t === 'amber' ? 'bg-amber-500' : 'bg-zinc-900 dark:bg-zinc-100'}`}
style={{ width: `${Math.min(100, r * 100)}%` }}
/>
<span aria-hidden className="absolute inset-y-0 w-px bg-white/70 dark:bg-zinc-950/70" style={{ left: '80%' }} />
</div>
{r >= 1 && <p className="mt-1.5 text-[12px] text-red-600 dark:text-red-400">Over by {fmt(m, m.used - m.limit)}. Overage is billed at the end of the period.</p>}
</li>
)
})}
</ul>
{worst >= 0.8 && onUpgrade && (
<div className={`mt-6 flex items-center justify-between gap-4 rounded-xl p-3.5 motion-safe:animate-[um-in_400ms_cubic-bezier(.22,1,.36,1)] ${worst >= 1 ? 'bg-red-50 dark:bg-red-500/10' : 'bg-amber-50 dark:bg-amber-500/10'}`}>
<p className="text-[13px] leading-snug">{worst >= 1 ? 'You’ve gone over a limit this period.' : 'You’re close to a limit.'} Upgrading raises every limit at once.</p>
<button type="button" onClick={onUpgrade} className="h-8 shrink-0 rounded-lg bg-zinc-900 px-3 text-[12.5px] font-semibold text-white dark:bg-white dark:text-zinc-900">
Upgrade
</button>
</div>
)}
<style>{`@keyframes um-in{from{opacity:0;transform:translateY(6px)}}`}</style>
</section>
)
}Overview
Usage that’s honest without being alarming. Each meter shows used out of limit, with a faint tick at 80%. Bars turn amber past the tick and red over the limit, where a line states the exact overage and when it is billed.
The upgrade prompt only appears once something is close to a limit, so it reads as help rather than an ad. The header says when the period resets. Numbers format compactly past 10,000 or with your own formatter, such as GB with one decimal.
Use it on billing and account pages and in usage-based pricing dashboards.
When to use
- Dashboards and reports where the shape of the data matters.
When not to use
- Exact lookups: give people a table or export too.
Installation
pnpm dlx shadcn@latest add https://sundaeui.dev/r/usage-meters.jsonnpx shadcn@latest add https://sundaeui.dev/r/usage-meters.jsonyarn dlx shadcn@latest add https://sundaeui.dev/r/usage-meters.jsonbunx --bun shadcn@latest add https://sundaeui.dev/r/usage-meters.jsonWorks in any React project. Files are written to components/sundae/. Read the source in the stage’s Code view.
- No dependencies beyond React 18 or later.
- Copy usage-meters.tsx into components/sundae/.
- Make sure Tailwind scans that folder (v4 detects it automatically; v3 needs it in `content`).
- Import the component and use it as shown below.
Usage
import { UsageMeters } from '@/components/sundae/usage-meters'
<UsageMeters
planName="Team"
resetsAt={period.end}
onUpgrade={() => router.push('/billing/upgrade')}
meters={[{ id: 'calls', label: 'API requests', used: 82400, limit: 100000 }]}
/>Props
| Prop | Type | Default |
|---|---|---|
meters*{ id, label, used, limit, unit?, format? }. | Meter[] | — |
resetsAtShows days until reset. | string (ISO) | — |
planNameShown in the header. | string | — |
onUpgradeEnables the upgrade prompt at 80% or more. | () => void | — |
Accessibility
- Each bar is a role="meter" labelled by its row, with a percentage value text.
- Colour is never the only signal: overage is also written out.
Customization
- Change the 0.8 warning threshold in tone() and the tick position.
FAQ
- Is Usage Meters free?
- Yes. Usage Meters is free for personal and commercial projects, with full source on this page.
- How do I install Usage Meters?
- Run npx shadcn@latest add https://sundaeui.dev/r/usage-meters.json. The files are written to components/sundae/. It has no dependencies beyond React 18 or later.
- Does Usage Meters support keyboard and screen readers?
- Each bar is a role="meter" labelled by its row, with a percentage value text. Colour is never the only signal: overage is also written out.
- Does Usage Meters respect reduced motion?
- Yes. With prefers-reduced-motion set, decorative motion is removed and state changes are instant.
More like this
All data →Pulse KPI
Data · UnderstatedData Table
Data · Understated- Pro
Kanban Board
Data · Understated - Pro
Team Members
Components · Understated Invoice List
Data · Refined- Pro
Seat Checkout
Checkout · Understated