Skip to content
Product block

Quota & Usage Meters

Consumption against plan limits, with warning thresholds, honest overage display and unmetered quotas that do not fake a bar.

172 lineslucide-reactAdded 8 Aug 2026Updated 2 Sept 2026
  • usage
  • quota
  • meter
  • limits
  • progress

What's included

  • components/usage-meter-panel.tsx
  • Needs lucide-react

Works with

  • React
  • Next.js
  • Tailwind CSS
  • TypeScript

npx hoverlab add usage-meter-panel

Or over MCP, from your editor's agent — no account needed.

License

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.

Was this useful?

Start a page with this section — add more, order them, and leave with the page source.

Preview

Usage this period

Resets 1 September

You are over the limit on at least one quota. Overage is billed at the end of the period.

  • API requests842,000 / 1,000,000

    84% of your limit used

  • Team seats8 / 10

    80% of your limit used

  • Storage47 GB / 50 GB

    94% of your limit used

  • Bandwidth128 GB / 100 GB

    28 GB over your limit

  • Projects23of unlimited

    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.

Source

components/usage-meter-panel.tsx
/**
 * <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>
  )
}

Before you paste

  • Styling is Tailwind utility classes on semantic tokens (bg-card, text-muted-foreground) — it inherits your theme instead of overriding it.
  • Install: npm i lucide-react
  • Every prop has a default, so it renders standalone before you wire it up.

Where it goes

Drop it at components/usage-meter-panel.tsx and import it where you need the section:

import { UsageMeterPanel } from '@/components/usage-meter-panel'

Customize

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.

Props

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.

PropTypeDefault
quotasQuota[]DEFAULT_QUOTAS
headingstring'Usage this period'
periodEndsstring'Resets 1 September'
warnAtFraction above which a quota is flagged as running out.number0.8
upgradeHrefstring—
classNamestring''

Not using React?

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.

usage-meter-panel.html
<!--
  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>
  • This is rendered HTML, not a translation of the React source. The Tailwind classes carry the design and work in any framework.
  • It is one frame: the component in its initial state, with no props applied beyond the defaults.
  • Requires Tailwind, and the design tokens the classes reference (bg-card, text-muted-foreground, and so on). The template ZIPs ship a globals.css that defines them.

What each framework gets across the whole catalog — effects convert properly; this rung is markup.

For AI

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.

See the prompt

Used in these pages

Want the whole screen instead of this one section? Open a page and copy it entire.

9 more blocks in Charts & Metrics

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 & Metrics

Shipping one commercially

Copying the code is free. Putting it in client work or a paid product is what Pro is for — the licence, not the access.

  • A commercial licence for everything in the catalog
  • Unlimited bundle exports, in Vue, Svelte and Tailwind
  • One payment — no subscription, nothing to renew
Pro — $79 once

More Charts & Metrics blocks

View category
Open the full page for this block

Sparkline Metric Cards

KPI cards with a hand-built SVG trend line — no charting dependency for a path this file computes in twelve lines.

Charts & Metrics142 linesNo deps
Open the full page for this block

CSS Bar Chart Panel

Categorical bars in plain CSS with gridlines, hover values and a screen-reader table carrying the real numbers.

Charts & Metrics139 linesNo deps
Open the full page for this block

Dual-Series Line Chart

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.

Charts & Metrics277 linesNo deps
Open the full page for this block

Donut Breakdown

Where a total went, drawn as one conic-gradient ring with the total in the hole and every number repeated in the legend.

Charts & Metrics205 linesNo deps
Open the full page for this block

Daily Activity Heatmap

A year of daily activity as one square per day, bucketed against a quantile so a single outlier cannot flatten the scale.

Charts & Metrics231 linesNo deps
Open the full page for this block

Conversion Funnel

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.

Charts & Metrics195 lines1 dep
Open the full page for this block

Metric Alert List

Alert thresholds with their current distance from firing, so a quiet alert is visibly armed rather than possibly broken.

Charts & Metrics141 linesNo deps
Open the full page for this block

KPI Summary Band

The top-of-dashboard figures, each carrying the comparison period that makes a delta mean anything.

Charts & Metrics118 linesNo deps
Open the full page for this block

Market Price Ticker

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.

Charts & Metrics180 lines1 dep