Plan & Next Charge Summary
Current plan, next charge with an actual figure and date, payment method, and a prominent pending-cancellation state.
Past invoices with tabular figures, worded statuses rather than bare coloured dots, and per-invoice download labels.
components/invoice-history-table.tsxnpx hoverlab add invoice-history-table
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.
Every charge on this account. PDFs are available for seven years.
Rendered live in your current theme — this is the same component whose source is below, not a screenshot of it.
/**
* <InvoiceHistoryTable> — past invoices with status and downloads.
*
* Money columns are `tabular-nums` and right-aligned, so the digits line up
* into columns you can scan down. Proportional figures in a currency column
* are the reason totals look wrong even when they are right.
*
* Status is a word plus a tone, never a bare coloured dot — "Failed" and
* "Refunded" are not the same event and a red circle cannot tell them
* apart. Each download link names its invoice in an `aria-label`, because a
* column of links all announced as "Download" is unnavigable.
*
* Server component.
*/
import * as React from 'react'
import { Download, ExternalLink } from 'lucide-react'
export type InvoiceStatus = 'paid' | 'open' | 'failed' | 'refunded'
export interface Invoice {
id: string
number: string
/** ISO date. */
date: string
description: string
amount: string
status: InvoiceStatus
pdfHref?: string
}
export interface InvoiceHistoryTableProps {
invoices?: Invoice[]
heading?: string
className?: string
}
const STATUS_TONE: Record<InvoiceStatus, string> = {
paid: 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400',
open: 'bg-sky-500/15 text-sky-600 dark:text-sky-400',
failed: 'bg-destructive/15 text-destructive',
refunded: 'bg-muted text-muted-foreground',
}
const STATUS_LABEL: Record<InvoiceStatus, string> = {
paid: 'Paid',
open: 'Due',
failed: 'Failed',
refunded: 'Refunded',
}
const DEFAULT_INVOICES: Invoice[] = [
{ id: '1', number: 'INV-2026-0812', date: '2026-08-01', description: 'Team plan — August', amount: '$490.00', status: 'paid' },
{ id: '2', number: 'INV-2026-0745', date: '2026-07-01', description: 'Team plan — July', amount: '$490.00', status: 'paid' },
{ id: '3', number: 'INV-2026-0698', date: '2026-06-14', description: 'Additional seats (3)', amount: '$147.00', status: 'refunded' },
{ id: '4', number: 'INV-2026-0671', date: '2026-06-01', description: 'Team plan — June', amount: '$490.00', status: 'failed' },
{ id: '5', number: 'INV-2026-0602', date: '2026-05-01', description: 'Pro plan — May', amount: '$190.00', status: 'paid' },
]
function formatDate(iso: string): string {
// Pinned to UTC — a bare `new Date('2026-08-01')` is midnight UTC and
// renders as 31 July for anyone west of Greenwich.
return new Date(`${iso}T00:00:00Z`).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
timeZone: 'UTC',
})
}
export function InvoiceHistoryTable({
invoices = DEFAULT_INVOICES,
heading = 'Invoice history',
className = '',
}: InvoiceHistoryTableProps) {
return (
<section
className={`overflow-hidden rounded-2xl border border-border/60 bg-card/80 backdrop-blur ${className}`}
>
<div className="flex flex-wrap items-center 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">
Every charge on this account. PDFs are available for seven years.
</p>
</div>
<a
href="#"
className="inline-flex items-center gap-1.5 text-sm font-medium text-muted-foreground transition-colors hover:text-foreground"
>
Billing portal
<ExternalLink aria-hidden className="h-3.5 w-3.5" />
</a>
</div>
{/*
`relative` is load-bearing. Tailwind's `sr-only` is `position:
absolute`, and an overflow box only clips an absolutely positioned
descendant when it is also that descendant's containing block.
Without it the "Download" labels in the last column laid out at their static position,
escaped this scroller and dragged the page sideways on a phone,
pointing at nothing anyone could see. Same fix as /compare.
*/}
<div className="relative overflow-x-auto">
<table className="w-full border-collapse text-sm">
<caption className="sr-only">{heading}</caption>
<thead>
<tr className="border-b border-border/60 bg-muted/30">
<th scope="col" className="px-6 py-3 text-start font-semibold">
Invoice
</th>
<th scope="col" className="px-6 py-3 text-start font-semibold">
Date
</th>
<th scope="col" className="px-6 py-3 text-start font-semibold">
Status
</th>
<th scope="col" className="px-6 py-3 text-end font-semibold">
Amount
</th>
<th scope="col" className="px-6 py-3 text-end font-semibold">
<span className="sr-only">Download</span>
</th>
</tr>
</thead>
<tbody>
{invoices.map((invoice) => (
<tr
key={invoice.id}
className="border-b border-border/40 last:border-0 transition-colors hover:bg-muted/20"
>
<th scope="row" className="px-6 py-3.5 text-start font-normal">
<span className="block font-mono text-xs font-medium">{invoice.number}</span>
<span className="block text-xs text-muted-foreground">
{invoice.description}
</span>
</th>
<td className="px-6 py-3.5 text-muted-foreground">
<time dateTime={invoice.date}>{formatDate(invoice.date)}</time>
</td>
<td className="px-6 py-3.5">
<span
className={`inline-flex rounded-full px-2 py-0.5 text-xs font-medium ${STATUS_TONE[invoice.status]}`}
>
{STATUS_LABEL[invoice.status]}
</span>
</td>
<td className="px-6 py-3.5 text-end font-medium tabular-nums">
{invoice.amount}
</td>
<td className="px-6 py-3.5 text-end">
<a
href={invoice.pdfHref ?? '#'}
aria-label={`Download invoice ${invoice.number} as PDF`}
className="inline-flex rounded-lg p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
>
<Download aria-hidden className="h-4 w-4" />
</a>
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
)
}
bg-card, text-muted-foreground) — it inherits your theme instead of overriding it.Drop it at components/invoice-history-table.tsx and import it where you need the section:
import { InvoiceHistoryTable } from '@/components/invoice-history-table'1 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 |
|---|---|---|
invoices | Invoice[] | DEFAULT_INVOICES |
heading | string | 'Invoice history' |
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.
<!--
Invoice History — 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-center justify-between gap-3 border-b border-border/60 px-6 py-4">
<div>
<h2 class="font-semibold tracking-tight">Invoice history</h2>
<p class="mt-0.5 text-sm text-muted-foreground">Every charge on this account. PDFs are available for seven years.</p>
</div>
<a href="#" class="inline-flex items-center gap-1.5 text-sm font-medium text-muted-foreground transition-colors hover:text-foreground">Billing portal<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-external-link h-3.5 w-3.5" aria-hidden="true"><path d="M15 3h6v6"></path><path d="M10 14 21 3"></path><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path></svg></a>
</div>
<div class="relative overflow-x-auto">
<table class="w-full border-collapse text-sm">
<caption class="sr-only">Invoice history</caption>
<thead>
<tr class="border-b border-border/60 bg-muted/30">
<th scope="col" class="px-6 py-3 text-start font-semibold">Invoice</th>
<th scope="col" class="px-6 py-3 text-start font-semibold">Date</th>
<th scope="col" class="px-6 py-3 text-start font-semibold">Status</th>
<th scope="col" class="px-6 py-3 text-end font-semibold">Amount</th>
<th scope="col" class="px-6 py-3 text-end font-semibold">
<span class="sr-only">Download</span>
</th>
</tr>
</thead>
<tbody>
<tr class="border-b border-border/40 last:border-0 transition-colors hover:bg-muted/20">
<th scope="row" class="px-6 py-3.5 text-start font-normal">
<span class="block font-mono text-xs font-medium">INV-2026-0812</span>
<span class="block text-xs text-muted-foreground">Team plan — August</span>
</th>
<td class="px-6 py-3.5 text-muted-foreground">
<time dateTime="2026-08-01">Aug 1, 2026</time>
</td>
<td class="px-6 py-3.5">
<span class="inline-flex rounded-full px-2 py-0.5 text-xs font-medium bg-emerald-500/15 text-emerald-600 dark:text-emerald-400">Paid</span>
</td>
<td class="px-6 py-3.5 text-end font-medium tabular-nums">$490.00</td>
<td class="px-6 py-3.5 text-end">
<a href="#" aria-label="Download invoice INV-2026-0812 as PDF" class="inline-flex rounded-lg p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-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-download h-4 w-4" 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>
</a>
</td>
</tr>
<tr class="border-b border-border/40 last:border-0 transition-colors hover:bg-muted/20">
<th scope="row" class="px-6 py-3.5 text-start font-normal">
<span class="block font-mono text-xs font-medium">INV-2026-0745</span>
<span class="block text-xs text-muted-foreground">Team plan — July</span>
</th>
<td class="px-6 py-3.5 text-muted-foreground">
<time dateTime="2026-07-01">Jul 1, 2026</time>
</td>
<td class="px-6 py-3.5">
<span class="inline-flex rounded-full px-2 py-0.5 text-xs font-medium bg-emerald-500/15 text-emerald-600 dark:text-emerald-400">Paid</span>
</td>
<td class="px-6 py-3.5 text-end font-medium tabular-nums">$490.00</td>
<td class="px-6 py-3.5 text-end">
<a href="#" aria-label="Download invoice INV-2026-0745 as PDF" class="inline-flex rounded-lg p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-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-download h-4 w-4" 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>
</a>
</td>
</tr>
<tr class="border-b border-border/40 last:border-0 transition-colors hover:bg-muted/20">
<th scope="row" class="px-6 py-3.5 text-start font-normal">
<span class="block font-mono text-xs font-medium">INV-2026-0698</span>
<span class="block text-xs text-muted-foreground">Additional seats (3)</span>
</th>
<td class="px-6 py-3.5 text-muted-foreground">
<time dateTime="2026-06-14">Jun 14, 2026</time>
</td>
<td class="px-6 py-3.5">
<span class="inline-flex rounded-full px-2 py-0.5 text-xs font-medium bg-muted text-muted-foreground">Refunded</span>
</td>
<td class="px-6 py-3.5 text-end font-medium tabular-nums">$147.00</td>
<td class="px-6 py-3.5 text-end">
<a href="#" aria-label="Download invoice INV-2026-0698 as PDF" class="inline-flex rounded-lg p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-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-download h-4 w-4" 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>
</a>
</td>
</tr>
<tr class="border-b border-border/40 last:border-0 transition-colors hover:bg-muted/20">
<th scope="row" class="px-6 py-3.5 text-start font-normal">
<span class="block font-mono text-xs font-medium">INV-2026-0671</span>
<span class="block text-xs text-muted-foreground">Team plan — June</span>
</th>
<td class="px-6 py-3.5 text-muted-foreground">
<time dateTime="2026-06-01">Jun 1, 2026</time>
</td>
<td class="px-6 py-3.5">
<span class="inline-flex rounded-full px-2 py-0.5 text-xs font-medium bg-destructive/15 text-destructive">Failed</span>
</td>
<td class="px-6 py-3.5 text-end font-medium tabular-nums">$490.00</td>
<td class="px-6 py-3.5 text-end">
<a href="#" aria-label="Download invoice INV-2026-0671 as PDF" class="inline-flex rounded-lg p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-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-download h-4 w-4" 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>
</a>
</td>
</tr>
<tr class="border-b border-border/40 last:border-0 transition-colors hover:bg-muted/20">
<th scope="row" class="px-6 py-3.5 text-start font-normal">
<span class="block font-mono text-xs font-medium">INV-2026-0602</span>
<span class="block text-xs text-muted-foreground">Pro plan — May</span>
</th>
<td class="px-6 py-3.5 text-muted-foreground">
<time dateTime="2026-05-01">May 1, 2026</time>
</td>
<td class="px-6 py-3.5">
<span class="inline-flex rounded-full px-2 py-0.5 text-xs font-medium bg-emerald-500/15 text-emerald-600 dark:text-emerald-400">Paid</span>
</td>
<td class="px-6 py-3.5 text-end font-medium tabular-nums">$190.00</td>
<td class="px-6 py-3.5 text-end">
<a href="#" aria-label="Download invoice INV-2026-0602 as PDF" class="inline-flex rounded-lg p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-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-download h-4 w-4" 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>
</a>
</td>
</tr>
</tbody>
</table>
</div>
</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 Billing & UsageCopying the code is free. Putting it in client work or a paid product is what Pro is for — the licence, not the access.
Current plan, next charge with an actual figure and date, payment method, and a prominent pending-cancellation state.
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.
Self-serve cancellation with no dark patterns: the exact date access ends, what breaks, one honest alternative, and a button that works.
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.
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.
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.
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.