PDFreact-print-pdf
DocsBrowser-only alpha

Statement of account

A multi-page bank statement with 64 transactions, repeating table header, and a closing-balance card glued to the end.

Edit this page

Live statement of account preview\u00b7 A4

StackBlitzGitHub Workshop
Click Export PDF to download a real vector PDF. The on-screen pixels match the PDF output exactly.
// 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>  );}
Live <PrintPreview> · A4Open in new tab
Pages are rendered inline as scaled DOM — no PDF round-trip. The same paginator drives 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-nums keeps 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

Full source on GitHub.

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() and moneySigned() with Intl.NumberFormat for locale-aware separators and currency symbols.
  • Date format. fmtDateLong is ISO-ish; for US conventions swap to Intl.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 with break-before: page and pass them all to a single exportToPDF call.

On this page