Sparkline Metric Cards
KPI cards with a hand-built SVG trend line — no charting dependency for a path this file computes in twelve lines.
Consumption against plan limits, with warning thresholds, honest overage display and unmetered quotas that do not fake a bar.
components/usage-meter-panel.tsxnpx hoverlab add usage-meter-panel
Or over MCP, from your editor's agent — no account needed.
Free to read, copy and install, for personal and non-commercial projects. Shipping it in client work or a paid product needs Pro ($79 once). The source lands in your repo and stops being ours — no attribution, nothing to upgrade.
Start a page with this section — add more, order them, and leave with the page source.
Resets 1 September
You are over the limit on at least one quota. Overage is billed at the end of the period.
84% of your limit used
80% of your limit used
94% of your limit used
28 GB over your limit
No limit on this plan
Rendered live in your current theme — this is the same component whose source is below, not a screenshot of it.
/**
* <UsageMeterPanel> — quota consumption against plan limits.
*
* The panel a user opens when they want to know whether they are about to
* be charged, so it is built to answer that first.
*
* - Bars change tone as they approach the limit, and the threshold is
* stated ("80% of your limit"), not just implied by turning amber.
* - Unlimited quotas render as a dash rather than a full bar. A metered
* bar for something with no ceiling is actively misleading.
* - Overage is drawn past the 100% mark in the destructive tone rather
* than clamped, because a bar pinned at full hides how far over you are.
*
* `<progress>` would be the semantic element, but it cannot be styled
* consistently across engines. The `role="progressbar"` div with explicit
* `aria-valuenow`/`min`/`max` carries the same information.
*
* Server component.
*/
import * as React from 'react'
import { TriangleAlert, Infinity as InfinityIcon } from 'lucide-react'
export interface Quota {
label: string
used: number
/** `null` for an unmetered allowance. */
limit: number | null
unit?: string
/** Formats both the used and limit figures. */
format?: (value: number) => string
}
export interface UsageMeterPanelProps {
quotas?: Quota[]
heading?: string
periodEnds?: string
/** Fraction above which a quota is flagged as running out. */
warnAt?: number
upgradeHref?: string
className?: string
}
const DEFAULT_QUOTAS: Quota[] = [
{ label: 'API requests', used: 842_000, limit: 1_000_000 },
{ label: 'Team seats', used: 8, limit: 10 },
{ label: 'Storage', used: 47, limit: 50, unit: 'GB' },
{ label: 'Bandwidth', used: 128, limit: 100, unit: 'GB' },
{ label: 'Projects', used: 23, limit: null },
]
export function UsageMeterPanel({
quotas = DEFAULT_QUOTAS,
heading = 'Usage this period',
periodEnds = 'Resets 1 September',
warnAt = 0.8,
upgradeHref,
className = '',
}: UsageMeterPanelProps) {
const anyOver = quotas.some((q) => q.limit !== null && q.used > q.limit)
return (
<section
className={`overflow-hidden rounded-2xl border border-border/60 bg-card/80 backdrop-blur ${className}`}
>
<div className="flex flex-wrap items-baseline justify-between gap-3 border-b border-border/60 px-6 py-4">
<div>
<h2 className="font-semibold tracking-tight">{heading}</h2>
<p className="mt-0.5 text-sm text-muted-foreground">{periodEnds}</p>
</div>
{upgradeHref ? (
<a
href={upgradeHref}
className="rounded-xl bg-primary px-3.5 py-2 text-sm font-semibold text-primary-foreground transition-colors hover:bg-primary/90"
>
Upgrade plan
</a>
) : null}
</div>
{anyOver ? (
<p className="flex items-start gap-2 border-b border-border/60 bg-destructive/5 px-6 py-3 text-sm text-destructive">
<TriangleAlert aria-hidden className="mt-0.5 h-4 w-4 shrink-0" />
You are over the limit on at least one quota. Overage is billed at the
end of the period.
</p>
) : null}
<ul className="divide-y divide-border/40">
{quotas.map((quota) => {
const format = quota.format ?? ((v: number) => v.toLocaleString('en-US'))
const unlimited = quota.limit === null
const fraction = unlimited ? 0 : quota.used / quota.limit!
const over = !unlimited && quota.used > quota.limit!
const warning = !unlimited && !over && fraction >= warnAt
const tone = over
? 'bg-destructive'
: warning
? 'bg-amber-500'
: 'bg-primary'
return (
<li key={quota.label} className="px-6 py-4">
<div className="flex items-baseline justify-between gap-3">
<span className="text-sm font-medium">{quota.label}</span>
<span className="text-sm tabular-nums text-muted-foreground">
{format(quota.used)}
{quota.unit ? ` ${quota.unit}` : ''}
{unlimited ? (
<span className="ms-1 inline-flex items-center gap-1 text-xs">
<span aria-hidden>/</span>
<InfinityIcon aria-hidden className="h-3.5 w-3.5" />
<span className="sr-only">of unlimited</span>
</span>
) : (
<>
{' / '}
{format(quota.limit!)}
{quota.unit ? ` ${quota.unit}` : ''}
</>
)}
</span>
</div>
{unlimited ? (
<p className="mt-2 text-xs text-muted-foreground">
No limit on this plan
</p>
) : (
<>
<div
role="progressbar"
aria-valuenow={Math.round(fraction * 100)}
aria-valuemin={0}
aria-valuemax={100}
aria-label={`${quota.label} usage`}
className="relative mt-2 h-2 overflow-hidden rounded-full bg-muted border border-transparent"
>
<span
className={`block h-full rounded-full border border-transparent transition-all ${tone}`}
style={{ width: `${Math.min(100, fraction * 100)}%` }}
/>
</div>
<p
className={`mt-1.5 text-xs ${
over
? 'font-medium text-destructive'
: warning
? 'font-medium text-amber-600 dark:text-amber-400'
: 'text-muted-foreground'
}`}
>
{over
? `${format(quota.used - quota.limit!)}${quota.unit ? ` ${quota.unit}` : ''} over your limit`
: warning
? `${Math.round(fraction * 100)}% of your limit used`
: `${Math.round(fraction * 100)}% used`}
</p>
</>
)}
</li>
)
})}
</ul>
</section>
)
}
bg-card, text-muted-foreground) — it inherits your theme instead of overriding it.Drop it at components/usage-meter-panel.tsx and import it where you need the section:
import { UsageMeterPanel } from '@/components/usage-meter-panel'3 of this block’s props are simple enough to drive from here. Change them and the block below re-renders — it is the same component whose source is above, not a mock of it. Everything else it accepts is in the table underneath.
Read out of the component’s own type and signature, so this cannot drift from the source below. Every prop has a default — the component renders standalone before you pass it anything.
| Prop | Type | Default |
|---|---|---|
quotas | Quota[] | DEFAULT_QUOTAS |
heading | string | 'Usage this period' |
periodEnds | string | 'Resets 1 September' |
warnAtFraction above which a quota is flagged as running out. | number | 0.8 |
upgradeHref | string | — |
className | string | '' |
The same block rendered once to markup, wrapped as a file your framework compiles. Tailwind classes are framework-agnostic, so the design transfers intact — the behaviour does not.
<!--
Quota & Usage Meters — markup from the Hoverlab catalog.
This is the block rendered once to HTML and wrapped as a component
file. It is not a port of the React source: the Tailwind classes carry
the design, which is the part that took the work, and they are the same
in every framework.
This block has no interactive behaviour, so nothing is missing.
-->
<section class="overflow-hidden rounded-2xl border border-border/60 bg-card/80 backdrop-blur ">
<div class="flex flex-wrap items-baseline justify-between gap-3 border-b border-border/60 px-6 py-4">
<div>
<h2 class="font-semibold tracking-tight">Usage this period</h2>
<p class="mt-0.5 text-sm text-muted-foreground">Resets 1 September</p>
</div>
</div>
<p class="flex items-start gap-2 border-b border-border/60 bg-destructive/5 px-6 py-3 text-sm text-destructive"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-triangle-alert mt-0.5 h-4 w-4 shrink-0" aria-hidden="true"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"></path><path d="M12 9v4"></path><path d="M12 17h.01"></path></svg>You are over the limit on at least one quota. Overage is billed at the end of the period.</p>
<ul class="divide-y divide-border/40">
<li class="px-6 py-4">
<div class="flex items-baseline justify-between gap-3">
<span class="text-sm font-medium">API requests</span>
<span class="text-sm tabular-nums text-muted-foreground">842,000 / 1,000,000</span>
</div>
<div role="progressbar" aria-valuenow="84" aria-valuemin="0" aria-valuemax="100" aria-label="API requests usage" class="relative mt-2 h-2 overflow-hidden rounded-full bg-muted border border-transparent">
<span class="block h-full rounded-full border border-transparent transition-all bg-amber-500" style="width:84.2%"></span>
</div>
<p class="mt-1.5 text-xs font-medium text-amber-600 dark:text-amber-400">84% of your limit used</p>
</li>
<li class="px-6 py-4">
<div class="flex items-baseline justify-between gap-3">
<span class="text-sm font-medium">Team seats</span>
<span class="text-sm tabular-nums text-muted-foreground">8 / 10</span>
</div>
<div role="progressbar" aria-valuenow="80" aria-valuemin="0" aria-valuemax="100" aria-label="Team seats usage" class="relative mt-2 h-2 overflow-hidden rounded-full bg-muted border border-transparent">
<span class="block h-full rounded-full border border-transparent transition-all bg-amber-500" style="width:80%"></span>
</div>
<p class="mt-1.5 text-xs font-medium text-amber-600 dark:text-amber-400">80% of your limit used</p>
</li>
<li class="px-6 py-4">
<div class="flex items-baseline justify-between gap-3">
<span class="text-sm font-medium">Storage</span>
<span class="text-sm tabular-nums text-muted-foreground">47 GB / 50 GB</span>
</div>
<div role="progressbar" aria-valuenow="94" aria-valuemin="0" aria-valuemax="100" aria-label="Storage usage" class="relative mt-2 h-2 overflow-hidden rounded-full bg-muted border border-transparent">
<span class="block h-full rounded-full border border-transparent transition-all bg-amber-500" style="width:94%"></span>
</div>
<p class="mt-1.5 text-xs font-medium text-amber-600 dark:text-amber-400">94% of your limit used</p>
</li>
<li class="px-6 py-4">
<div class="flex items-baseline justify-between gap-3">
<span class="text-sm font-medium">Bandwidth</span>
<span class="text-sm tabular-nums text-muted-foreground">128 GB / 100 GB</span>
</div>
<div role="progressbar" aria-valuenow="128" aria-valuemin="0" aria-valuemax="100" aria-label="Bandwidth usage" class="relative mt-2 h-2 overflow-hidden rounded-full bg-muted border border-transparent">
<span class="block h-full rounded-full border border-transparent transition-all bg-destructive" style="width:100%"></span>
</div>
<p class="mt-1.5 text-xs font-medium text-destructive">28 GB over your limit</p>
</li>
<li class="px-6 py-4">
<div class="flex items-baseline justify-between gap-3">
<span class="text-sm font-medium">Projects</span>
<span class="text-sm tabular-nums text-muted-foreground">23<span class="ms-1 inline-flex items-center gap-1 text-xs"><span aria-hidden="true">/</span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-infinity h-3.5 w-3.5" aria-hidden="true"><path d="M6 16c5 0 7-8 12-8a4 4 0 0 1 0 8c-5 0-7-8-12-8a4 4 0 1 0 0 8"></path></svg><span class="sr-only">of unlimited</span></span></span>
</div>
<p class="mt-2 text-xs text-muted-foreground">No limit on this plan</p>
</li>
</ul>
</section>
What each framework gets across the whole catalog — effects convert properly; this rung is markup.
The component, its props, the design tokens it expects and the command that installs it — as one prompt. Paste it into Claude, Cursor, v0 or ChatGPT and what they build around it will match the rest of the catalog instead of inventing its own system.
Want the whole screen instead of this one section? Open a page and copy it entire.
All of them free to read, copy and install — no account, no locked tiles, no watermarked preview. The whole catalog is open, and so are the API and the CLI.
Browse Charts & MetricsCopying the code is free. Putting it in client work or a paid product is what Pro is for — the licence, not the access.
KPI cards with a hand-built SVG trend line — no charting dependency for a path this file computes in twelve lines.
Categorical bars in plain CSS with gridlines, hover values and a screen-reader table carrying the real numbers.
A year-over-year trend in inline SVG with a printed axis, no charting dependency and the same data repeated as a screen-reader table.
Where a total went, drawn as one conic-gradient ring with the total in the hole and every number repeated in the legend.
A year of daily activity as one square per day, bucketed against a quantile so a single outlier cannot flatten the scale.
A funnel that leads with the step-to-step rate and the people who fell out of it, not the tidy share-of-top percentages nobody can act on. Biggest drop named in words, and a footnote saying which definition of funnel this is.
Alert thresholds with their current distance from firing, so a quiet alert is visibly armed rather than possibly broken.
The top-of-dashboard figures, each carrying the comparison period that makes a delta mean anything.
The moving price rail that sits under an exchange’s navbar — direction carried by an arrow and a sign as well as by colour, and the duplicate half dropped rather than read twice.