Skip to content
Product 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.

265 lineslucide-reactAdded 31 Aug 2026Updated 2 Sept 2026
  • billing
  • invoice
  • proration
  • tax
  • receipt

What's included

  • components/billing-invoice-detail.tsx
  • Needs lucide-react

Works with

  • React
  • Next.js
  • Tailwind CSS
  • TypeScript

npx hoverlab add billing-invoice-detail

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

Invoice INV-2026-0841

paid

Issued 31 August 2026 · Due 14 September 2026

Download PDF

Billed to

Acme Corp14 Bridgewater StreetManchester M1 5ANUnited Kingdom
Invoice number
INV-2026-0841
Tax registration
GB 123 4567 89
Payment method
Visa ending 4242
DescriptionQtyUnitAmount
Studio plan — 40 seats1 Aug – 31 Aug 202640$12.00$480.00
Unused time on Team plan20 Aug – 31 Aug 202624$12.00-$103.20
Remaining time on Studio plan20 Aug – 31 Aug 202640$12.00$172.00
AI credit pack — 2,000 credits1$15.00$15.00
Subtotal$563.80
VAT (United Kingdom) at 20%$112.76
Total$676.56

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

Source

components/billing-invoice-detail.tsx
/**
 * <BillingInvoiceDetail> — one invoice, with the arithmetic shown.
 *
 * The catalog has an invoice *history* table. It lists invoices and links
 * to nothing, because until now there was nothing to link to. This is the
 * page that link goes to, and it is the only billing surface where the
 * numbers have to reconcile against a bank statement rather than merely
 * look plausible.
 *
 * PRORATION IS THE WHOLE DIFFICULTY
 *
 * Nearly every support ticket about an invoice is a mid-cycle plan change,
 * and nearly every invoice UI renders it as one opaque line. Here the
 * credit and the charge are separate lines with their own date ranges,
 * because "why is this £41.29" is answered by seeing the eleven unused days
 * that were credited back — and by nothing else.
 *
 * TAX IS A LINE, NOT A FOOTNOTE. The rate, the jurisdiction and the
 * customer's registration number all appear, because in the EU and the UK
 * an invoice without them is not a valid invoice, and a component that
 * omits them quietly makes its user non-compliant.
 *
 * A SERVER COMPONENT. There is nothing interactive here — an invoice is a
 * record, not a form — so it ships with no `'use client'` and hydrates
 * nothing. The download button is a plain link in real use.
 *
 * ACCESSIBILITY: a real `<table>` with a `<tfoot>` carrying the totals, so
 * the summary is associated with the line items rather than floating beside
 * them; `<dl>` for the invoice metadata, which is what a description list
 * is for.
 */

import * as React from 'react'
import { Download, FileText } from 'lucide-react'

export interface InvoiceLine {
  id: string
  description: string
  /** Date range this line covers, already formatted. */
  period?: string
  quantity: number
  unitCents: number
  amountCents: number
  /** Rendered as a credit — negative amounts read badly without it. */
  credit?: boolean
}

export interface BillingInvoiceDetailProps {
  number?: string
  issuedOn?: string
  dueOn?: string
  status?: 'paid' | 'open' | 'past due'
  billedTo?: string[]
  taxLabel?: string
  taxRate?: number
  vatNumber?: string
  lines?: InvoiceLine[]
  className?: string
}

const DEFAULT_LINES: InvoiceLine[] = [
  {
    id: 'studio',
    description: 'Studio plan — 40 seats',
    period: '1 Aug – 31 Aug 2026',
    quantity: 40,
    unitCents: 1200,
    amountCents: 48_000,
  },
  {
    id: 'proration-credit',
    description: 'Unused time on Team plan',
    period: '20 Aug – 31 Aug 2026',
    quantity: 24,
    unitCents: 1200,
    amountCents: -10_320,
    credit: true,
  },
  {
    id: 'proration-charge',
    description: 'Remaining time on Studio plan',
    period: '20 Aug – 31 Aug 2026',
    quantity: 40,
    unitCents: 1200,
    amountCents: 17_200,
  },
  {
    id: 'credits',
    description: 'AI credit pack — 2,000 credits',
    quantity: 1,
    unitCents: 1500,
    amountCents: 1500,
  },
]

const money = (cents: number) =>
  (cents / 100).toLocaleString('en-US', { style: 'currency', currency: 'USD' })

const STATUS_STYLE: Record<NonNullable<BillingInvoiceDetailProps['status']>, string> = {
  paid: 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400',
  open: 'bg-muted text-muted-foreground',
  'past due': 'bg-destructive/10 text-destructive',
}

export function BillingInvoiceDetail({
  number = 'INV-2026-0841',
  issuedOn = '31 August 2026',
  dueOn = '14 September 2026',
  status = 'paid',
  billedTo = ['Acme Corp', '14 Bridgewater Street', 'Manchester M1 5AN', 'United Kingdom'],
  taxLabel = 'VAT (United Kingdom)',
  taxRate = 0.2,
  vatNumber = 'GB 123 4567 89',
  lines = DEFAULT_LINES,
  className = '',
}: BillingInvoiceDetailProps) {
  const subtotal = lines.reduce((sum, line) => sum + line.amountCents, 0)
  const tax = Math.round(subtotal * taxRate)
  const total = subtotal + tax

  return (
    <section
      className={`rounded-2xl border border-border bg-card text-card-foreground ${className}`}
    >
      <header className="flex flex-wrap items-start justify-between gap-4 border-b border-border p-5 sm:p-6">
        <div>
          <div className="flex flex-wrap items-center gap-2">
            <FileText aria-hidden className="h-4 w-4 text-muted-foreground" />
            <h2 className="text-base font-semibold">Invoice {number}</h2>
            <span
              className={`rounded px-1.5 py-0.5 text-[11px] font-semibold uppercase tracking-wide ${STATUS_STYLE[status]}`}
            >
              {status}
            </span>
          </div>
          <p className="mt-1 text-sm text-muted-foreground">
            Issued {issuedOn} · Due {dueOn}
          </p>
        </div>

        <a
          href="#"
          className="inline-flex items-center gap-1.5 rounded-lg border border-border px-3 py-1.5 text-sm font-medium transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
        >
          <Download aria-hidden className="h-3.5 w-3.5" />
          Download PDF
        </a>
      </header>

      <div className="grid gap-6 border-b border-border p-5 sm:grid-cols-2 sm:p-6">
        <div>
          <h3
            data-stress-ignore
            className="text-xs font-semibold uppercase tracking-wider text-muted-foreground"
          >
            Billed to
          </h3>
          <address className="mt-2 text-sm not-italic leading-relaxed">
            {/* Keyed by position: an address is a fixed list of display
                lines, and two of them can legitimately read the same (or be
                blank). Keying by content drops the duplicate. */}
            {billedTo.map((line, i) => (
              <span key={i} className="block">
                {line}
              </span>
            ))}
          </address>
        </div>

        <dl className="space-y-2 text-sm">
          <div className="flex justify-between gap-4">
            <dt className="text-muted-foreground">Invoice number</dt>
            <dd className="font-mono">{number}</dd>
          </div>
          <div className="flex justify-between gap-4">
            <dt className="text-muted-foreground">Tax registration</dt>
            <dd className="font-mono">{vatNumber}</dd>
          </div>
          <div className="flex justify-between gap-4">
            <dt className="text-muted-foreground">Payment method</dt>
            <dd>Visa ending 4242</dd>
          </div>
        </dl>
      </div>

      <div className="overflow-x-auto">
        <table className="w-full min-w-[36rem] border-collapse text-start text-sm">
          <thead>
            <tr className="border-b border-border">
              <th scope="col" className="px-5 py-2.5 font-semibold sm:px-6">
                Description
              </th>
              <th scope="col" className="px-5 py-2.5 text-end font-semibold">
                Qty
              </th>
              <th scope="col" className="px-5 py-2.5 text-end font-semibold">
                Unit
              </th>
              <th scope="col" className="px-5 py-2.5 text-end font-semibold sm:px-6">
                Amount
              </th>
            </tr>
          </thead>

          <tbody>
            {lines.map((line) => (
              <tr key={line.id} className="border-b border-border/60">
                <th scope="row" className="px-5 py-3 font-medium sm:px-6">
                  {line.description}
                  {/*
                    The date range is what makes a proration line legible.
                    Without it the credit and the charge look like a
                    duplicate billing.
                  */}
                  {line.period ? (
                    <span className="mt-0.5 block text-xs font-normal text-muted-foreground">
                      {line.period}
                    </span>
                  ) : null}
                </th>
                <td className="px-5 py-3 text-end tabular-nums text-muted-foreground">
                  {line.quantity}
                </td>
                <td className="px-5 py-3 text-end tabular-nums text-muted-foreground">
                  {money(line.unitCents)}
                </td>
                <td
                  className={`px-5 py-3 text-end tabular-nums sm:px-6 ${
                    line.credit ? 'text-emerald-600 dark:text-emerald-400' : ''
                  }`}
                >
                  {money(line.amountCents)}
                </td>
              </tr>
            ))}
          </tbody>

          <tfoot>
            <tr>
              <th scope="row" colSpan={3} className="px-5 pt-4 text-end font-normal text-muted-foreground sm:px-6">
                Subtotal
              </th>
              <td className="px-5 pt-4 text-end tabular-nums sm:px-6">{money(subtotal)}</td>
            </tr>
            <tr>
              <th scope="row" colSpan={3} className="px-5 py-1.5 text-end font-normal text-muted-foreground sm:px-6">
                {taxLabel} at {(taxRate * 100).toFixed(0)}%
              </th>
              <td className="px-5 py-1.5 text-end tabular-nums sm:px-6">{money(tax)}</td>
            </tr>
            <tr className="border-t border-border">
              <th scope="row" colSpan={3} className="px-5 py-3 text-end font-semibold sm:px-6">
                Total
              </th>
              <td className="px-5 py-3 text-end text-base font-semibold tabular-nums sm:px-6">
                {money(total)}
              </td>
            </tr>
          </tfoot>
        </table>
      </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/billing-invoice-detail.tsx and import it where you need the section:

import { BillingInvoiceDetail } from '@/components/billing-invoice-detail'

Customize

7 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
numberstring'INV-2026-0841'
issuedOnstring'31 August 2026'
dueOnstring'14 September 2026'
status'paid' | 'open' | 'past due''paid'
billedTostring[]['Acme Corp', '14 Bridgewater Street', 'Manch…
taxLabelstring'VAT (United Kingdom)'
taxRatenumber0.2
vatNumberstring'GB 123 4567 89'
linesInvoiceLine[]DEFAULT_LINES
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.

billing-invoice-detail.html
<!--
  Invoice Detail — 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 class="rounded-2xl border border-border bg-card text-card-foreground ">
  <header class="flex flex-wrap items-start justify-between gap-4 border-b border-border p-5 sm:p-6">
    <div>
      <div class="flex flex-wrap items-center gap-2">
        <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-file-text h-4 w-4 text-muted-foreground" aria-hidden="true">
          <path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"></path>
          <path d="M14 2v4a2 2 0 0 0 2 2h4"></path>
          <path d="M10 9H8"></path>
          <path d="M16 13H8"></path>
          <path d="M16 17H8"></path>
        </svg>
        <h2 class="text-base font-semibold">Invoice INV-2026-0841</h2>
        <span class="rounded px-1.5 py-0.5 text-[11px] font-semibold uppercase tracking-wide bg-emerald-500/10 text-emerald-600 dark:text-emerald-400">paid</span>
      </div>
      <p class="mt-1 text-sm text-muted-foreground">Issued 31 August 2026 · Due 14 September 2026</p>
    </div>
    <a href="#" class="inline-flex items-center gap-1.5 rounded-lg border border-border px-3 py-1.5 text-sm font-medium transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"><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-download h-3.5 w-3.5" aria-hidden="true"><path d="M12 15V3"></path><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><path d="m7 10 5 5 5-5"></path></svg>Download PDF</a>
  </header>
  <div class="grid gap-6 border-b border-border p-5 sm:grid-cols-2 sm:p-6">
    <div>
      <h3 data-stress-ignore="true" class="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Billed to</h3>
      <address class="mt-2 text-sm not-italic leading-relaxed">
        <span class="block">Acme Corp</span>
        <span class="block">14 Bridgewater Street</span>
        <span class="block">Manchester M1 5AN</span>
        <span class="block">United Kingdom</span>
      </address>
    </div>
    <dl class="space-y-2 text-sm">
      <div class="flex justify-between gap-4">
        <dt class="text-muted-foreground">Invoice number</dt>
        <dd class="font-mono">INV-2026-0841</dd>
      </div>
      <div class="flex justify-between gap-4">
        <dt class="text-muted-foreground">Tax registration</dt>
        <dd class="font-mono">GB 123 4567 89</dd>
      </div>
      <div class="flex justify-between gap-4">
        <dt class="text-muted-foreground">Payment method</dt>
        <dd>Visa ending 4242</dd>
      </div>
    </dl>
  </div>
  <div class="overflow-x-auto">
    <table class="w-full min-w-[36rem] border-collapse text-start text-sm">
      <thead>
        <tr class="border-b border-border">
          <th scope="col" class="px-5 py-2.5 font-semibold sm:px-6">Description</th>
          <th scope="col" class="px-5 py-2.5 text-end font-semibold">Qty</th>
          <th scope="col" class="px-5 py-2.5 text-end font-semibold">Unit</th>
          <th scope="col" class="px-5 py-2.5 text-end font-semibold sm:px-6">Amount</th>
        </tr>
      </thead>
      <tbody>
        <tr class="border-b border-border/60">
          <th scope="row" class="px-5 py-3 font-medium sm:px-6">Studio plan — 40 seats<span class="mt-0.5 block text-xs font-normal text-muted-foreground">1 Aug – 31 Aug 2026</span></th>
          <td class="px-5 py-3 text-end tabular-nums text-muted-foreground">40</td>
          <td class="px-5 py-3 text-end tabular-nums text-muted-foreground">$12.00</td>
          <td class="px-5 py-3 text-end tabular-nums sm:px-6 ">$480.00</td>
        </tr>
        <tr class="border-b border-border/60">
          <th scope="row" class="px-5 py-3 font-medium sm:px-6">Unused time on Team plan<span class="mt-0.5 block text-xs font-normal text-muted-foreground">20 Aug – 31 Aug 2026</span></th>
          <td class="px-5 py-3 text-end tabular-nums text-muted-foreground">24</td>
          <td class="px-5 py-3 text-end tabular-nums text-muted-foreground">$12.00</td>
          <td class="px-5 py-3 text-end tabular-nums sm:px-6 text-emerald-600 dark:text-emerald-400">-$103.20</td>
        </tr>
        <tr class="border-b border-border/60">
          <th scope="row" class="px-5 py-3 font-medium sm:px-6">Remaining time on Studio plan<span class="mt-0.5 block text-xs font-normal text-muted-foreground">20 Aug – 31 Aug 2026</span></th>
          <td class="px-5 py-3 text-end tabular-nums text-muted-foreground">40</td>
          <td class="px-5 py-3 text-end tabular-nums text-muted-foreground">$12.00</td>
          <td class="px-5 py-3 text-end tabular-nums sm:px-6 ">$172.00</td>
        </tr>
        <tr class="border-b border-border/60">
          <th scope="row" class="px-5 py-3 font-medium sm:px-6">AI credit pack — 2,000 credits</th>
          <td class="px-5 py-3 text-end tabular-nums text-muted-foreground">1</td>
          <td class="px-5 py-3 text-end tabular-nums text-muted-foreground">$15.00</td>
          <td class="px-5 py-3 text-end tabular-nums sm:px-6 ">$15.00</td>
        </tr>
      </tbody>
      <tfoot>
        <tr>
          <th scope="row" colSpan="3" class="px-5 pt-4 text-end font-normal text-muted-foreground sm:px-6">Subtotal</th>
          <td class="px-5 pt-4 text-end tabular-nums sm:px-6">$563.80</td>
        </tr>
        <tr>
          <th scope="row" colSpan="3" class="px-5 py-1.5 text-end font-normal text-muted-foreground sm:px-6">VAT (United Kingdom) at 20%</th>
          <td class="px-5 py-1.5 text-end tabular-nums sm:px-6">$112.76</td>
        </tr>
        <tr class="border-t border-border">
          <th scope="row" colSpan="3" class="px-5 py-3 text-end font-semibold sm:px-6">Total</th>
          <td class="px-5 py-3 text-end text-base font-semibold tabular-nums sm:px-6">$676.56</td>
        </tr>
      </tfoot>
    </table>
  </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

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.

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

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