Skip to content
Product block

Overage Notice with Projection

Past the included allowance, with the end-of-month projection and every number the invoice will use — because a surprise bill is a product defect.

230 lineslucide-reactAdded 25 Aug 2026Updated 2 Sept 2026
  • overage
  • usage
  • billing
  • quota
  • projection

What's included

  • components/usage-overage-notice.tsx
  • Needs lucide-react

Works with

  • React
  • Next.js
  • Tailwind CSS
  • TypeScript

npx hoverlab add usage-overage-notice

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

You are past your included API requests

Everything is still running. You are being billed for the overage, and this is what it looks like so far.

1,402,500 of 1,000,000 requests140%
Over your allowance
402,500 requests
Overage rate
$0.12 per 10,000
Charged so far
$4.83
At this rate, by day 30
$12.042,003,571 requests

Projected from 66,786 requests a day over 21 days so far. It is a straight line, not a forecast — a quiet week moves it.

Upgrading is usually cheaper than paying overage at this volume. Capping stops the charges and starts returning 429s — some teams genuinely prefer that, and it is a real option rather than a threat.

Rendered live in your current theme — this is the same component whose source is below, not a screenshot of it.

Source

components/usage-overage-notice.tsx
'use client'

/**
 * <UsageOverageNotice> — the bill is going to be bigger, said before it is.
 *
 * <UsageMeterPanel> shows consumption against a limit. This is the state
 * after that limit is passed, and it is a different component because it
 * has a different job: not reporting a number, but preventing the support
 * ticket that starts "why was I charged $340 this month".
 *
 * The rule this is built on is that a surprise invoice is a product defect.
 * Every metered product eventually learns it, usually from a refund queue.
 *
 * WHAT MAKES IT USEFUL RATHER THAN ALARMING
 *
 *   The projection, not just the overage. "You are 40% over" is a fact
 *   about the past. "At this rate you will finish the month at 1.9M and be
 *   billed about $87 extra" is the thing a person can act on, and it is the
 *   number they would otherwise have to work out with a calculator.
 *
 *   The arithmetic is shown. Included, used, over, rate, total. A charge
 *   nobody can reproduce is a charge somebody disputes, and every one of
 *   these numbers is already on the invoice — hiding them here only delays
 *   the argument.
 *
 *   Both real exits, side by side. Upgrading is usually cheaper than paying
 *   overage, and saying so costs a little margin and buys the thing this
 *   screen exists for. A cap is the other honest answer: some people would
 *   genuinely rather the API start failing than be billed, and a product
 *   that only offers "pay more" is not offering a choice.
 *
 * DEGRADES, DOES NOT DRAMATISE
 *
 * Amber, not red, and no icon that reads as an outage. Nothing is broken —
 * the service is still running, which is precisely why it is costing money.
 * A destructive-red banner here teaches people to ignore destructive-red
 * banners.
 *
 * The projection recomputes from the day of the month rather than being a
 * fixed number, so the same component is honest on the 3rd and on the 28th,
 * when the remaining-days multiplier is the whole story.
 */

import * as React from 'react'
import { ArrowUpRight, Gauge, ShieldCheck, TrendingUp } from 'lucide-react'

export interface UsageOverageNoticeProps {
  metricLabel?: string
  /** What the plan includes, in `unit`s. */
  included?: number
  /** Consumed so far this period. */
  used?: number
  /** Day of the billing period, 1-based. */
  dayOfPeriod?: number
  periodDays?: number
  /** Cost per overage unit, in the currency below. */
  ratePerUnit?: number
  /** Overage units the rate is quoted per — 1,000 requests, 1 GB, and so on. */
  rateUnitSize?: number
  unit?: string
  currencySymbol?: string
  onUpgrade?: () => void
  onCap?: () => void
  className?: string
}

export function UsageOverageNotice({
  metricLabel = 'API requests',
  included = 1_000_000,
  used = 1_402_500,
  dayOfPeriod = 21,
  periodDays = 30,
  ratePerUnit = 0.12,
  rateUnitSize = 10_000,
  unit = 'requests',
  currencySymbol = '$',
  onUpgrade,
  onCap,
  className = '',
}: UsageOverageNoticeProps) {
  /*
    Per-instance prefix for every id this block emits.

    The literals these replaced were a latent duplicate the moment the
    block appeared twice on one document, and `aria-labelledby` on a
    duplicated id resolves to the first match -- so the second copy was
    labelled by the first copy's heading. Client component, so `useId` is
    the right tool.
  */
  const uid = React.useId()

  const over = Math.max(0, used - included)

  /*
    Straight-line projection from the run rate so far. Deliberately the
    simplest model that is defensible: anything cleverer (weekday
    weighting, trend fitting) produces a number the customer cannot check,
    and a projection nobody can reproduce is worth less than a rough one
    they can.
  */
  const perDay = dayOfPeriod > 0 ? used / dayOfPeriod : 0
  const projected = Math.round(perDay * periodDays)
  const projectedOver = Math.max(0, projected - included)
  const projectedCharge = (projectedOver / rateUnitSize) * ratePerUnit
  const chargeSoFar = (over / rateUnitSize) * ratePerUnit

  const pct = included > 0 ? Math.round((used / included) * 100) : 0
  const money = (value: number) =>
    `${currencySymbol}${value.toLocaleString('en-US', {
      minimumFractionDigits: 2,
      maximumFractionDigits: 2,
    })}`
  const count = (value: number) => value.toLocaleString('en-US')

  return (
    <section
      aria-labelledby={`${uid}-overage-heading`}
      className={`mx-auto w-full max-w-2xl px-4 py-16 sm:px-6 lg:px-8 ${className}`}
    >
      {/* Amber, not red. Nothing is broken. */}
      <div className="rounded-2xl border border-amber-500/40 bg-amber-500/5 p-6 sm:p-7">
        <div className="flex items-start gap-3">
          <Gauge aria-hidden className="mt-0.5 h-5 w-5 shrink-0 text-amber-600 dark:text-amber-500" />
          <div className="min-w-0">
            {/* The label is used as written. Lower-casing it to fit the
                sentence turns "API requests" into "api requests", and every
                metric worth metering is an acronym sooner or later. */}
            <h2 id={`${uid}-overage-heading`} className="text-base font-semibold text-foreground">
              You are past your included {metricLabel}
            </h2>
            <p className="mt-1 text-sm text-muted-foreground">
              Everything is still running. You are being billed for the
              overage, and this is what it looks like so far.
            </p>
          </div>
        </div>

        {/* The bar goes past 100% and shows it, rather than pinning full and
            hiding the size of the problem. */}
        <div className="mt-5">
          <div className="flex items-baseline justify-between text-sm">
            <span className="text-muted-foreground">
              {count(used)} of {count(included)} {unit}
            </span>
            <span className="font-mono font-medium text-foreground">{pct}%</span>
          </div>
          <div className="mt-2 h-2.5 overflow-hidden rounded-full bg-muted">
            <div className="flex h-full">
              <div
                className="h-full bg-primary border border-transparent"
                style={{ width: `${(included / Math.max(used, included)) * 100}%` }}
              />
              <div
                className="h-full bg-amber-500 border border-transparent"
                style={{ width: `${(over / Math.max(used, included)) * 100}%` }}
              />
            </div>
          </div>
        </div>

        {/* Every number on the invoice, reproducible. */}
        <dl className="mt-5 divide-y divide-border/60 rounded-xl border border-border bg-card px-4">
          <div className="flex items-baseline justify-between gap-4 py-2.5 text-sm">
            <dt className="text-muted-foreground">Over your allowance</dt>
            <dd className="font-mono text-foreground">
              {count(over)} {unit}
            </dd>
          </div>
          <div className="flex items-baseline justify-between gap-4 py-2.5 text-sm">
            <dt className="text-muted-foreground">Overage rate</dt>
            <dd className="font-mono text-foreground">
              {money(ratePerUnit)} per {count(rateUnitSize)}
            </dd>
          </div>
          <div className="flex items-baseline justify-between gap-4 py-2.5 text-sm">
            <dt className="text-muted-foreground">Charged so far</dt>
            <dd className="font-mono font-medium text-foreground">{money(chargeSoFar)}</dd>
          </div>
          <div className="flex items-baseline justify-between gap-4 py-2.5 text-sm">
            <dt className="flex items-center gap-1.5 text-muted-foreground">
              <TrendingUp aria-hidden className="h-3.5 w-3.5" />
              At this rate, by day {periodDays}
            </dt>
            <dd className="text-end">
              <span className="block font-mono font-semibold text-foreground">
                {money(projectedCharge)}
              </span>
              <span className="block font-mono text-xs text-muted-foreground">
                {count(projected)} {unit}
              </span>
            </dd>
          </div>
        </dl>

        <p className="mt-3 text-xs text-muted-foreground">
          Projected from {count(Math.round(perDay))} {unit} a day over {dayOfPeriod}{' '}
          {dayOfPeriod === 1 ? 'day' : 'days'} so far. It is a straight line, not a
          forecast — a quiet week moves it.
        </p>

        {/* Two real exits. The cheaper one is not buried. */}
        <div className="mt-5 flex flex-wrap gap-3">
          <button
            type="button"
            onClick={onUpgrade}
            className="inline-flex h-9 items-center gap-1.5 rounded-lg bg-primary px-4 text-sm font-semibold text-primary-foreground transition hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
          >
            <ArrowUpRight aria-hidden className="h-4 w-4" />
            Move to a plan that includes this
          </button>
          <button
            type="button"
            onClick={onCap}
            className="inline-flex h-9 items-center gap-1.5 rounded-lg border border-border bg-background px-4 text-sm font-medium text-foreground transition hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
          >
            <ShieldCheck aria-hidden className="h-4 w-4" />
            Cap usage instead
          </button>
        </div>

        <p className="mt-3 text-xs text-muted-foreground">
          Upgrading is usually cheaper than paying overage at this volume.
          Capping stops the charges and starts returning 429s — some teams
          genuinely prefer that, and it is a real option rather than a threat.
        </p>
      </div>
    </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-overage-notice.tsx and import it where you need the section:

import { UsageOverageNotice } from '@/components/usage-overage-notice'

Customize

6 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
metricLabelstring'API requests'
includedWhat the plan includes, in `unit`s.number1_000_000
usedConsumed so far this period.number1_402_500
dayOfPeriodDay of the billing period, 1-based.number21
periodDaysnumber30
ratePerUnitCost per overage unit, in the currency below.number0.12
rateUnitSizeOverage units the rate is quoted per — 1,000 requests, 1 GB, and so on.number10_000
unitstring'requests'
currencySymbolstring'$'
onUpgrade() => void—
onCap() => void—
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.

This block is interactive. The markup below is its initial state with the event handlers stripped — you will need to re-wire the behaviour in your framework.

usage-overage-notice.html
<!--
  Overage Notice with Projection — 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 is interactive in React and the handlers are NOT here.
  Buttons, toggles and menus render in their initial state and do
  nothing until you wire them up.
-->
<section aria-labelledby="_R_0_-overage-heading" class="mx-auto w-full max-w-2xl px-4 py-16 sm:px-6 lg:px-8 ">
  <div class="rounded-2xl border border-amber-500/40 bg-amber-500/5 p-6 sm:p-7">
    <div class="flex items-start gap-3">
      <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-gauge mt-0.5 h-5 w-5 shrink-0 text-amber-600 dark:text-amber-500" aria-hidden="true">
        <path d="m12 14 4-4"></path>
        <path d="M3.34 19a10 10 0 1 1 17.32 0"></path>
      </svg>
      <div class="min-w-0">
        <h2 id="_R_0_-overage-heading" class="text-base font-semibold text-foreground">You are past your included API requests</h2>
        <p class="mt-1 text-sm text-muted-foreground">Everything is still running. You are being billed for the overage, and this is what it looks like so far.</p>
      </div>
    </div>
    <div class="mt-5">
      <div class="flex items-baseline justify-between text-sm">
        <span class="text-muted-foreground">1,402,500 of 1,000,000 requests</span>
        <span class="font-mono font-medium text-foreground">140%</span>
      </div>
      <div class="mt-2 h-2.5 overflow-hidden rounded-full bg-muted">
        <div class="flex h-full">
          <div class="h-full bg-primary border border-transparent" style="width:71.301247771836%"></div>
          <div class="h-full bg-amber-500 border border-transparent" style="width:28.698752228163993%"></div>
        </div>
      </div>
    </div>
    <dl class="mt-5 divide-y divide-border/60 rounded-xl border border-border bg-card px-4">
      <div class="flex items-baseline justify-between gap-4 py-2.5 text-sm">
        <dt class="text-muted-foreground">Over your allowance</dt>
        <dd class="font-mono text-foreground">402,500 requests</dd>
      </div>
      <div class="flex items-baseline justify-between gap-4 py-2.5 text-sm">
        <dt class="text-muted-foreground">Overage rate</dt>
        <dd class="font-mono text-foreground">$0.12 per 10,000</dd>
      </div>
      <div class="flex items-baseline justify-between gap-4 py-2.5 text-sm">
        <dt class="text-muted-foreground">Charged so far</dt>
        <dd class="font-mono font-medium text-foreground">$4.83</dd>
      </div>
      <div class="flex items-baseline justify-between gap-4 py-2.5 text-sm">
        <dt class="flex items-center gap-1.5 text-muted-foreground"><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-trending-up h-3.5 w-3.5" aria-hidden="true"><path d="M16 7h6v6"></path><path d="m22 7-8.5 8.5-5-5L2 17"></path></svg>At this rate, by day 30</dt>
        <dd class="text-end">
          <span class="block font-mono font-semibold text-foreground">$12.04</span>
          <span class="block font-mono text-xs text-muted-foreground">2,003,571 requests</span>
        </dd>
      </div>
    </dl>
    <p class="mt-3 text-xs text-muted-foreground">Projected from 66,786 requests a day over 21 days so far. It is a straight line, not a forecast — a quiet week moves it.</p>
    <div class="mt-5 flex flex-wrap gap-3">
      <button type="button" class="inline-flex h-9 items-center gap-1.5 rounded-lg bg-primary px-4 text-sm font-semibold text-primary-foreground transition hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"><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-arrow-up-right h-4 w-4" aria-hidden="true"><path d="M7 7h10v10"></path><path d="M7 17 17 7"></path></svg>Move to a plan that includes this</button>
      <button type="button" class="inline-flex h-9 items-center gap-1.5 rounded-lg border border-border bg-background px-4 text-sm font-medium text-foreground transition hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"><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-shield-check h-4 w-4" aria-hidden="true"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"></path><path d="m9 12 2 2 4-4"></path></svg>Cap usage instead</button>
    </div>
    <p class="mt-3 text-xs text-muted-foreground">Upgrading is usually cheaper than paying overage at this volume. Capping stops the charges and starts returning 429s — some teams genuinely prefer that, and it is a real option rather than a threat.</p>
  </div>
</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.
  • This block is interactive in React — toggles, menus or form state. None of that survives here; the markup is the closed/default state and the handlers are gone. Re-wire them in your own framework.
  • 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.

7 more blocks in Billing & Usage

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 Billing & Usage

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 Billing & Usage blocks

View category
Open the full page for this block

Plan & Next Charge Summary

Current plan, next charge with an actual figure and date, payment method, and a prominent pending-cancellation state.

Billing & Usage151 lines1 dep
Open the full page for this block

Invoice History

Past invoices with tabular figures, worded statuses rather than bare coloured dots, and per-invoice download labels.

Billing & Usage174 lines1 dep
Open the full page for this block

Payment Methods On File

Cards on file with the two failures that cost the subscription: expiry called out before it fails, and the default stated on the row rather than implied by ordering.

Billing & Usage248 lines1 dep
Open the full page for this block

Subscription Cancel Flow

Self-serve cancellation with no dark patterns: the exact date access ends, what breaks, one honest alternative, and a button that works.

Billing & Usage273 lines1 dep
Open the full page for this block

Seat Count & Proration

Changing seats with the bill shown first: the part-period charge and the new recurring amount as two numbers, the arithmetic spelled out, and the asymmetry stated where it bites — removing a seat refunds nothing today.

Billing & Usage254 lines1 dep
Open the full page for this block

Invoice Detail

One invoice with the arithmetic shown: proration split into a credit and a charge with their own date ranges, tax as a line with its jurisdiction, and totals in a real tfoot.

Billing & Usage265 lines1 dep
Open the full page for this block

Credit Balance and Expiry

Consumable credits split into the buckets they actually live in, rendered in spend order, with a proportional warning that only appears when a meaningful balance is about to expire.

Billing & Usage222 lines1 dep