Statement of account
A multi-page bank statement with 64 transactions, repeating table header, and a closing-balance card glued to the end.
// Statement of account — multi-page financial report.//// QA targets:// - 2.1 repeat-band scope: the table's <thead data-print-repeat> repeats// across continuation pages, but the closing-balance summary card AFTER// the table sits flush against the last row (no phantom header gap).// - Currency formatting with negative amounts.// - 60+ rows — 3 pages on A4, ~2-3 pages on US Letter.import { Rand, fmtDate, fmtDateLong, makeAddress, makePerson, makeTransactions, money, moneySigned,} from "./lib/data";export function StatementFixture() { const r = new Rand(20260101); const customer = makePerson(r); const billing = makeAddress(r); const { opening, closing, rows } = makeTransactions(20260101, 64, 4280.0); const periodStart = rows[0]!.date; const periodEnd = rows[rows.length - 1]!.date; const totalCredits = rows.filter((t) => t.amount > 0).reduce((s, t) => s + t.amount, 0); const totalDebits = rows.filter((t) => t.amount < 0).reduce((s, t) => s + t.amount, 0); return ( <article className="space-y-6 p-10 text-gray-900"> {/* Letterhead */} <header className="flex items-start justify-between border-b-2 border-gray-900 pb-5"> <div> <p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-gray-500"> Cobalt Bank · Personal accounts </p> <h1 className="mt-1 text-2xl font-semibold tracking-tight">Statement of account</h1> <p className="mt-1 text-xs text-gray-600"> Period: {fmtDateLong(periodStart)} — {fmtDateLong(periodEnd)} </p> </div> <div className="text-right text-xs leading-5 text-gray-700"> <p className="font-semibold text-gray-900">Account no.</p> <p className="font-mono">04-3919-{20260101 % 100000}</p> <p className="mt-2 font-semibold text-gray-900">Issued</p> <p>{fmtDateLong(new Date(2026, 4, 4))}</p> </div> </header> {/* Customer + summary band */} <section className="grid grid-cols-2 gap-6"> <div className="rounded-md border border-gray-200 bg-white p-4"> <p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-gray-500"> Account holder </p> <p className="mt-2 text-sm font-medium">{customer.full}</p> <p className="mt-1 text-xs leading-5 text-gray-600"> {billing.street} <br /> {billing.city}, {billing.region} {billing.postal} <br /> {billing.country} </p> <p className="mt-2 text-xs text-gray-500"> {customer.email} · {customer.phone} </p> </div> <div className="rounded-md border-l-4 border-y border-r border-gray-200 border-l-emerald-600 bg-emerald-50/30 p-4"> <p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-emerald-700"> Closing balance </p> <p className="mt-1 font-mono text-3xl font-semibold tabular-nums text-gray-900"> {money(closing)} </p> <dl className="mt-4 grid grid-cols-2 gap-x-4 gap-y-1 text-xs"> <dt className="text-gray-500">Opening</dt> <dd className="text-right font-mono tabular-nums">{money(opening)}</dd> <dt className="text-gray-500">Credits</dt> <dd className="text-right font-mono tabular-nums text-emerald-700"> {money(totalCredits)} </dd> <dt className="text-gray-500">Debits</dt> <dd className="text-right font-mono tabular-nums text-red-700"> {moneySigned(totalDebits)} </dd> </dl> </div> </section> {/* Transactions — repeating header */} <section> <h2 className="mb-2 text-sm font-semibold uppercase tracking-[0.14em] text-gray-700"> Transactions </h2> <table className="w-full border-collapse text-xs"> <thead data-print-repeat> <tr className="border-y border-gray-300 text-left text-[10px] uppercase tracking-wider text-gray-500"> <th className="py-2 pr-3 font-medium">Date</th> <th className="py-2 pr-3 font-medium">Reference</th> <th className="py-2 pr-3 font-medium">Description</th> <th className="py-2 pr-3 font-medium">Category</th> <th className="py-2 pr-3 text-right font-medium">Amount</th> <th className="py-2 text-right font-medium">Balance</th> </tr> </thead> <tbody> {rows.map((t) => ( <tr key={t.ref} className="border-b border-gray-100 align-top"> <td className="py-1.5 pr-3 font-mono text-gray-700">{fmtDate(t.date)}</td> <td className="py-1.5 pr-3 font-mono text-gray-500">{t.ref}</td> <td className="py-1.5 pr-3">{t.description}</td> <td className="py-1.5 pr-3 text-gray-500">{t.category}</td> <td className={`py-1.5 pr-3 text-right font-mono tabular-nums ${ t.amount < 0 ? "text-red-700" : "text-gray-900" }`} > {moneySigned(t.amount)} </td> <td className="py-1.5 text-right font-mono tabular-nums text-gray-700"> {money(t.balance)} </td> </tr> ))} </tbody> </table> </section> {/* Closing summary — sits flush against the table thanks to 2.1 fix */} <section className="rounded-md border border-gray-900 bg-gray-50 p-5"> <div className="flex items-center justify-between"> <div> <p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-gray-600"> Closing balance carried forward </p> <p className="mt-1 text-xs text-gray-600"> {rows.length} transactions · {fmtDateLong(periodStart)} — {fmtDateLong(periodEnd)} </p> </div> <p className="font-mono text-2xl font-semibold tabular-nums">{money(closing)}</p> </div> <hr className="my-4 border-gray-300" /> <p className="text-[11px] leading-5 text-gray-600"> Please review your statement carefully. Discrepancies must be reported within 30 days of the issue date. Visit cobaltbank.example/statements for the digital copy. </p> </section> <p className="text-[10px] text-gray-400">Cobalt Bank, NA · Member XYZ · cobaltbank.example</p> </article> );}exportToPDF, so the page breaks you see here are the page breaks you'll get in the file.What you're looking at
A monthly statement \u2014 letterhead, account holder card, KPI summary, 64 transactions across three pages, and a closing-balance card at the end. This fixture exists in the workshop specifically to exercise the trickiest part of pagination: a table that flows across pages followed by content that must sit flush against the last row.
Pagination here demonstrates:
- 64 rows, ~3 A4 pages. The paginator splits on row boundaries.
<thead data-print-repeat>that repeats across continuation pages. Every page shows Date \u00b7 Reference \u00b7 Description \u00b7 Category \u00b7 Amount \u00b7 Balance.- No phantom gap after the table. The closing-balance card sits directly below the last transaction row, even when the table ends mid-page. (This was a real bug we fixed in v0.2 \u2014 the QA target 2.1 comment in the fixture flags it.)
- Negative amounts in red, positive in default. Plain Tailwind conditional classes. The walker reads computed colors per cell.
Other features in play:
- Letterhead with
border-b-2. Two-pixel borders survive the walker without rounding artifacts. - Left-accent KPI card. The closing-balance card has a 4px green left border (
border-l-4 border-l-emerald-600). Per-side colored borders are vector. - Tabular nums for currency columns.
tabular-numskeeps decimals aligned regardless of digit count.
The exporter call
Statements are operationally similar to invoices. Headers and footers stay simple \u2014 a one-line band with the account number and page count:
const exporter = usePDFExport({
fileName: `statement-${period}.pdf`,
paperSize: "A4",
margins: { top: 48, right: 40, bottom: 40, left: 40 },
header: ({ pageNumber, totalPages }) => (
<div className="flex justify-between border-b border-gray-200 pb-2 text-[10px] text-gray-500">
<span>Cobalt Bank \u00b7 Account 04-3919-\u2026</span>
<span>Page {pageNumber} of {totalPages}</span>
</div>
),
headerHeight: 28,
});The component
The three structural sections:
export function Statement({ customer, rows, opening, closing }) {
const totalCredits = rows.filter(t => t.amount > 0).reduce((s, t) => s + t.amount, 0);
const totalDebits = rows.filter(t => t.amount < 0).reduce((s, t) => s + t.amount, 0);
return (
<article className="space-y-6 p-10 text-gray-900">
{/* 1. Letterhead */}
<header className="flex items-start justify-between border-b-2 border-gray-900 pb-5">
\u2026
</header>
{/* 2. Customer + summary band */}
<section className="grid grid-cols-2 gap-6">
<CustomerCard customer={customer} billing={billing} />
<ClosingBalanceCard
closing={closing}
opening={opening}
credits={totalCredits}
debits={totalDebits}
/>
</section>
{/* 3. The transactions table \u2014 the one that paginates */}
<section>
<h2 className="\u2026">Transactions</h2>
<table className="w-full border-collapse text-xs">
<thead data-print-repeat>
<tr className="\u2026">
<th>Date</th><th>Reference</th><th>Description</th>
<th>Category</th><th>Amount</th><th>Balance</th>
</tr>
</thead>
<tbody>
{rows.map(t => <TransactionRow key={t.ref} t={t} />)}
</tbody>
</table>
</section>
{/* 4. Closing balance carried forward \u2014 sits flush below the table */}
<section className="rounded-md border border-gray-900 bg-gray-50 p-5">
\u2026
</section>
</article>
);
}Why the choices look the way they do
<thead data-print-repeat> instead of CSS only
Same reason as the invoice example. The data-attribute signal is unambiguous; we don't depend on print CSS being honored consistently across whatever bundling pipeline you happen to use.
One closing-balance card before the table AND one after
The card at the top is the headline number, the card at the bottom is the legal carry-forward. Banks have done it this way for decades because the reader sees the answer immediately at the top, and the legally-binding figure (with the period statement and "report discrepancies within 30 days" notice) appears at the close. The library handles both because they're just sections in document order.
align-top on <tr>
Multi-line descriptions in some transactions ("PURCHASE AUTH XXXXXXXX VISA \u2026") wrap to two lines. Without align-top, the date and reference would float to vertical center, which looks off. Try removing it once and you'll see what we mean.
Why categories are styled as text-gray-500, not branded chips
Chips look great on screen and ridiculous on a statement someone is going to print and file. The fixture deliberately keeps "PDF business document" aesthetics rather than "SaaS dashboard" aesthetics. Branding belongs in the letterhead.
Variations
- Currency. Replace
money()andmoneySigned()withIntl.NumberFormatfor locale-aware separators and currency symbols. - Date format.
fmtDateLongis ISO-ish; for US conventions swap toIntl.DateTimeFormat("en-US", { dateStyle: "long" }). - Per-row tax columns. Add columns to the table; the repeating header automatically picks them up.
- Multiple accounts in one PDF. Wrap each
<Statement>in a section withbreak-before: pageand pass them all to a singleexportToPDFcall.
Related
- Pagination \u2014
<thead data-print-repeat>and continuation behavior - Best practices \u2014 tabular nums, width-matching
- Production checklist \u2014 pre-launch sweep for financial documents
Invoice (A4 multi-page)
A B2B invoice with 50 line items that paginates across three A4 pages, with header / footer that repeat and a totals card that sits flush at the end.
Financial summary report
A quarterly summary with KPI cards, three charts (vector + raster fallback), and a transactions table that paginates with a repeating header.