Skip to content
Marketing block

Prepaid Credit Packs

One-time packs with the per-credit price worked out for the reader and the saving stated rather than implied — the pricing shape for a product that sells quantity, not a subscription.

218 lineslucide-reactAdded 24 Aug 2026Updated 11 Sept 2026
  • pricing
  • credits
  • one-time
  • packs
  • prepaid

What's included

  • components/pricing-credits.tsx
  • Needs lucide-react

Works with

  • React
  • Next.js
  • Tailwind CSS
  • TypeScript

npx hoverlab add pricing-credits

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

Credits

Buy what you need, once

No plan to join and nothing that renews. Top up whenever the balance runs low, or never again.

One credit is one generation at standard quality. High quality costs four.

  • 500 credits

    $5

    $0.010 per credit

    Enough to finish an evaluation.

    Buy 500
  • Most bought

    2,000 credits

    $15Save 25%

    $0.0075 per credit

    What a working month looks like for one person.

    Buy 2,000
  • 10,000 credits

    $60Save 40%

    $0.006 per credit

    For a small team, or a batch job with a deadline.

    Buy 10,000
  • Credits never expire
  • No subscription, no card kept on file
  • Unused credits are refundable for 30 days

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

Source

components/pricing-credits.tsx
/**
 * <PricingCredits> — packs you buy once, not a plan you join.
 *
 * Every other pricing block in this catalog assumes a subscription.
 * <PricingTiers> toggles monthly against yearly, <PricingSingle> sells one
 * recurring plan, <PricingUsageCalculator> projects a monthly bill and
 * <ComparisonTable> compares plans. None of them can express the thing an
 * AI product, a rendering service or an API most often sells: a quantity,
 * bought once, spent whenever.
 *
 * The shape matters commercially, not just visually. Prepaid packs are how
 * you sell to someone who cannot get a recurring charge approved — a
 * contractor, a student, a team in a market where cross-border subscriptions
 * decline at the bank — and to the much larger group who simply will not
 * start a subscription to try something.
 *
 * The unit price is the part people leave off, and leaving it off is what
 * makes a pack grid feel like a trick. Four prices with four credit counts
 * is arithmetic the reader has to do to find the discount you are offering
 * them, and most will not; they will assume the largest pack is the rip-off,
 * because on most sites it is. `unit` is a required field here for that
 * reason, and the saving against the smallest pack is stated rather than
 * implied by a badge.
 *
 * Expiry is stated at the top rather than in a footnote. It is the first
 * question anyone asks about prepaid anything, and burying it is how a page
 * that is telling the truth ends up looking like one that is not.
 *
 * A <ul> of cards rather than a table: packs are alternatives, not rows of
 * one comparison, and each is independently purchasable.
 */

import * as React from 'react'
import { Check, Sparkles } from 'lucide-react'

export interface CreditPack {
  credits: string
  price: string
  /** Price per credit, worked out for the reader. Required on purpose. */
  unit: string
  /** e.g. "Save 25%" — against the smallest pack, stated not implied. */
  saving?: string
  note?: string
  featured?: boolean
  ctaLabel?: string
  ctaHref?: string
}

export interface PricingCreditsProps {
  eyebrow?: string
  heading?: string
  subheading?: string
  packs?: CreditPack[]
  /** What a credit buys. Without this the prices mean nothing. */
  unitExplainer?: string
  assurances?: string[]
  className?: string
}

const DEFAULT_PACKS: CreditPack[] = [
  {
    credits: '500 credits',
    price: '$5',
    unit: '$0.010 per credit',
    note: 'Enough to finish an evaluation.',
    ctaLabel: 'Buy 500',
  },
  {
    credits: '2,000 credits',
    price: '$15',
    unit: '$0.0075 per credit',
    saving: 'Save 25%',
    note: 'What a working month looks like for one person.',
    featured: true,
    ctaLabel: 'Buy 2,000',
  },
  {
    credits: '10,000 credits',
    price: '$60',
    unit: '$0.006 per credit',
    saving: 'Save 40%',
    note: 'For a small team, or a batch job with a deadline.',
    ctaLabel: 'Buy 10,000',
  },
]

const DEFAULT_ASSURANCES = [
  'Credits never expire',
  'No subscription, no card kept on file',
  'Unused credits are refundable for 30 days',
]

/*
  Per-instance id, hashed from props that differ between instances.

  A literal id is a latent duplicate the moment this block is rendered
  twice on one document -- two pages on a catalog hub, or one page using
  the section twice. `aria-labelledby` pointing at a duplicated id resolves
  to whichever element comes first, so the second copy is announced with
  the first copy's label. Server component, so no `useId`: hashing props
  gives each instance its own target and stays stable across server and
  client renders in a way a counter would not.
*/
function instanceId(...parts: (string | undefined)[]): string {
  const text = parts.filter(Boolean).join('|')
  let hash = 0
  for (let i = 0; i < text.length; i++) hash = (Math.imul(hash, 31) + text.charCodeAt(i)) | 0
  return (hash >>> 0).toString(36).slice(0, 6)
}

export function PricingCredits({
  eyebrow = 'Credits',
  heading = 'Buy what you need, once',
  subheading = 'No plan to join and nothing that renews. Top up whenever the balance runs low, or never again.',
  packs = DEFAULT_PACKS,
  unitExplainer = 'One credit is one generation at standard quality. High quality costs four.',
  assurances = DEFAULT_ASSURANCES,
  className = '',
}: PricingCreditsProps) {
  const uid = instanceId(heading)

  return (
    <section
      aria-labelledby={`pricing-credits-heading-${uid}`}
      className={`mx-auto w-full max-w-6xl px-4 py-16 sm:px-6 lg:px-8 ${className}`}
    >
      <div className="mx-auto max-w-2xl text-center">
        <p className="text-sm font-medium uppercase tracking-wider text-primary">{eyebrow}</p>
        <h2
          id={`pricing-credits-heading-${uid}`}
          className="mt-3 text-balance text-3xl font-bold tracking-tight text-foreground sm:text-4xl"
        >
          {heading}
        </h2>
        <p className="mt-4 text-pretty leading-relaxed text-muted-foreground">{subheading}</p>

        {unitExplainer ? (
          <p className="mt-6 inline-block rounded-lg bg-muted px-4 py-2 text-sm text-muted-foreground">
            {unitExplainer}
          </p>
        ) : null}
      </div>

      <ul className="mt-12 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
        {packs.map((pack) => (
          <li
            key={pack.credits}
            className={`relative flex flex-col rounded-2xl border bg-card p-8 ${
              pack.featured ? 'border-primary ring-1 ring-primary' : 'border-border/60'
            }`}
          >
            {pack.featured ? (
              <span className="absolute -top-3 start-8 inline-flex items-center gap-1 rounded-full bg-primary px-3 py-1 text-xs font-semibold text-primary-foreground">
                <Sparkles aria-hidden className="h-3.5 w-3.5" />
                Most bought
              </span>
            ) : null}

            {pack.credits ? (
              <h3
                data-stress-ignore
                className="break-words text-sm font-semibold uppercase tracking-wider text-muted-foreground"
              >
                {pack.credits}
              </h3>
            ) : null}

            {/* Wraps: a long price and a saving pill together are wider than
                the card once the grid is three across. */}
            <p className="mt-4 flex flex-wrap items-baseline gap-3">
              <span className="text-4xl font-bold tracking-tight text-foreground">
                {pack.price}
              </span>
              {pack.saving ? (
                <span className="rounded-full bg-primary/10 px-2.5 py-1 text-xs font-semibold text-primary">
                  {pack.saving}
                </span>
              ) : null}
            </p>

            <p className="mt-2 break-words text-sm tabular-nums text-muted-foreground">{pack.unit}</p>

            {pack.note ? (
              <p className="mt-4 flex-1 break-words text-pretty text-sm leading-relaxed text-muted-foreground">
                {pack.note}
              </p>
            ) : (
              <span className="flex-1" />
            )}

            <a
              href={pack.ctaHref ?? '#'}
              className={`mt-8 inline-flex h-11 items-center justify-center rounded-lg px-5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background ${
                pack.featured
                  ? 'bg-primary text-primary-foreground hover:bg-primary/90'
                  : 'border border-border text-foreground hover:bg-muted'
              }`}
            >
              {pack.ctaLabel ?? `Buy ${pack.credits}`}
            </a>
          </li>
        ))}
      </ul>

      {assurances.length > 0 ? (
        <ul className="mt-10 flex flex-wrap items-center justify-center gap-x-8 gap-y-3">
          {assurances.map((assurance) => (
            <li key={assurance} className="flex items-center gap-2 text-sm text-muted-foreground">
              <Check aria-hidden className="h-4 w-4 shrink-0 text-primary" />
              {assurance}
            </li>
          ))}
        </ul>
      ) : null}
    </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/pricing-credits.tsx and import it where you need the section:

import { PricingCredits } from '@/components/pricing-credits'

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
eyebrowstring'Credits'
headingstring'Buy what you need, once'
subheadingstring—
packsCreditPack[]DEFAULT_PACKS
unitExplainerWhat a credit buys. Without this the prices mean nothing.string'One credit is one generation at standard qua…
assurancesstring[]DEFAULT_ASSURANCES
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.

pricing-credits.html
<!--
  Prepaid Credit Packs — 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 aria-labelledby="pricing-credits-heading-1djs98" class="mx-auto w-full max-w-6xl px-4 py-16 sm:px-6 lg:px-8 ">
  <div class="mx-auto max-w-2xl text-center">
    <p class="text-sm font-medium uppercase tracking-wider text-primary">Credits</p>
    <h2 id="pricing-credits-heading-1djs98" class="mt-3 text-balance text-3xl font-bold tracking-tight text-foreground sm:text-4xl">Buy what you need, once</h2>
    <p class="mt-4 text-pretty leading-relaxed text-muted-foreground">No plan to join and nothing that renews. Top up whenever the balance runs low, or never again.</p>
    <p class="mt-6 inline-block rounded-lg bg-muted px-4 py-2 text-sm text-muted-foreground">One credit is one generation at standard quality. High quality costs four.</p>
  </div>
  <ul class="mt-12 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
    <li class="relative flex flex-col rounded-2xl border bg-card p-8 border-border/60">
      <h3 data-stress-ignore="true" class="break-words text-sm font-semibold uppercase tracking-wider text-muted-foreground">500 credits</h3>
      <p class="mt-4 flex flex-wrap items-baseline gap-3">
        <span class="text-4xl font-bold tracking-tight text-foreground">$5</span>
      </p>
      <p class="mt-2 break-words text-sm tabular-nums text-muted-foreground">$0.010 per credit</p>
      <p class="mt-4 flex-1 break-words text-pretty text-sm leading-relaxed text-muted-foreground">Enough to finish an evaluation.</p>
      <a href="#" class="mt-8 inline-flex h-11 items-center justify-center rounded-lg px-5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background border border-border text-foreground hover:bg-muted">Buy 500</a>
    </li>
    <li class="relative flex flex-col rounded-2xl border bg-card p-8 border-primary ring-1 ring-primary">
      <span class="absolute -top-3 start-8 inline-flex items-center gap-1 rounded-full bg-primary px-3 py-1 text-xs font-semibold text-primary-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-sparkles h-3.5 w-3.5" aria-hidden="true"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"></path><path d="M20 3v4"></path><path d="M22 5h-4"></path><path d="M4 17v2"></path><path d="M5 18H3"></path></svg>Most bought</span>
      <h3 data-stress-ignore="true" class="break-words text-sm font-semibold uppercase tracking-wider text-muted-foreground">2,000 credits</h3>
      <p class="mt-4 flex flex-wrap items-baseline gap-3">
        <span class="text-4xl font-bold tracking-tight text-foreground">$15</span>
        <span class="rounded-full bg-primary/10 px-2.5 py-1 text-xs font-semibold text-primary">Save 25%</span>
      </p>
      <p class="mt-2 break-words text-sm tabular-nums text-muted-foreground">$0.0075 per credit</p>
      <p class="mt-4 flex-1 break-words text-pretty text-sm leading-relaxed text-muted-foreground">What a working month looks like for one person.</p>
      <a href="#" class="mt-8 inline-flex h-11 items-center justify-center rounded-lg px-5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background bg-primary text-primary-foreground hover:bg-primary/90">Buy 2,000</a>
    </li>
    <li class="relative flex flex-col rounded-2xl border bg-card p-8 border-border/60">
      <h3 data-stress-ignore="true" class="break-words text-sm font-semibold uppercase tracking-wider text-muted-foreground">10,000 credits</h3>
      <p class="mt-4 flex flex-wrap items-baseline gap-3">
        <span class="text-4xl font-bold tracking-tight text-foreground">$60</span>
        <span class="rounded-full bg-primary/10 px-2.5 py-1 text-xs font-semibold text-primary">Save 40%</span>
      </p>
      <p class="mt-2 break-words text-sm tabular-nums text-muted-foreground">$0.006 per credit</p>
      <p class="mt-4 flex-1 break-words text-pretty text-sm leading-relaxed text-muted-foreground">For a small team, or a batch job with a deadline.</p>
      <a href="#" class="mt-8 inline-flex h-11 items-center justify-center rounded-lg px-5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background border border-border text-foreground hover:bg-muted">Buy 10,000</a>
    </li>
  </ul>
  <ul class="mt-10 flex flex-wrap items-center justify-center gap-x-8 gap-y-3">
    <li class="flex items-center gap-2 text-sm 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-check h-4 w-4 shrink-0 text-primary" aria-hidden="true"><path d="M20 6 9 17l-5-5"></path></svg>Credits never expire</li>
    <li class="flex items-center gap-2 text-sm 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-check h-4 w-4 shrink-0 text-primary" aria-hidden="true"><path d="M20 6 9 17l-5-5"></path></svg>No subscription, no card kept on file</li>
    <li class="flex items-center gap-2 text-sm 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-check h-4 w-4 shrink-0 text-primary" aria-hidden="true"><path d="M20 6 9 17l-5-5"></path></svg>Unused credits are refundable for 30 days</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.

7 more blocks in Pricing

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 Pricing

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 Pricing blocks

View category
Open the full page for this block

Three-Plan Pricing Toggle

Three plans with a monthly/yearly switch, yearly prices derived from the monthly figure so the two can never disagree.

Pricing192 lines1 dep
Open the full page for this block

Single-Plan Price Card

One price given a whole card, with the objection a one-time price raises answered under the button rather than in a FAQ further down.

Pricing145 lines1 dep
Open the full page for this block

Tiered Usage Calculator

A slider that turns a rate card into the number a buyer actually cares about, with a per-tier breakdown so the total can be checked rather than taken on trust.

Pricing224 linesNo deps
Open the full page for this block

Plan Comparison Matrix

A real table with scoped headers, a sticky first column and screen-reader text behind every check and dash.

Pricing145 lines1 dep
Open the full page for this block

In-App Plan Picker

A native radiogroup of plans with the current one disabled and the prorated charge announced as it changes — the upgrade dialog, not the marketing table.

Pricing242 lines1 dep
Open the full page for this block

Pricing Value Split

The case for the price, made beside the price — what the alternative costs, in the units a buyer already measures.

Pricing158 linesNo deps
Open the full page for this block

Plan Limits List

What actually runs out on each plan, including what happens when it does — the row a pricing table leaves off.

Pricing149 linesNo deps