Multi-page PDF Best Practices: Headers, Footers, and Page Numbers

A practical guide to building reliable multi-page PDFs with consistent headers, footers, and page numbers — including layout pitfalls, repeating elements, and how to handle "Page X of Y" cleanly.

pdfheadersfooterspaginationnode.jshtml-to-pdf

Multi-page PDF Best Practices: Headers, Footers, and Page Numbers

Single-page PDFs are easy. Once you cross into multi-page territory — invoices that span 12 pages, contracts, statements, reports — small layout decisions start compounding into real bugs. Footers overlap content. Page numbers reset mid-document. Headers disappear on page breaks. This post is a practical checklist for getting multi-page PDFs right, regardless of which renderer you're using.

Reserve space for headers and footers before you render

The most common multi-page bug is content flowing underneath a fixed header or footer. This happens because most HTML-to-PDF engines render the body at full page height, then paint the header and footer on top.

The fix is simple: tell the renderer how much vertical space to reserve. In CSS-based renderers (Chromium, WeasyPrint, Kamy), @page margins are the source of truth.

css
@page { size: A4; margin: 80px 40px 60px 40px; /* top right bottom left */ }

That top margin of 80px is the space your header lives in. The 60px bottom is for your footer. Body content will automatically flow inside the remaining area and break cleanly across pages.

A good rule of thumb:

  • Header height: 60–80px (room for a logo + one line of metadata)
  • Footer height: 40–60px (room for page numbers + small print)

If your header or footer changes height depending on data (e.g. a long client name), pick the worst case and reserve for that.

Use running elements, not absolute positioning

Avoid position: absolute for headers and footers in multi-page documents. It works for page one, then breaks because absolute positioning is relative to the containing block, not the page.

Instead, use the CSS Paged Media spec, which most modern PDF engines support:

css
@page { margin: 80px 40px 60px 40px; @top-left { content: element(pageHeader); } @bottom-center { content: "Page " counter(page) " of " counter(pages); font-size: 10px; color: #666; } } .page-header { position: running(pageHeader); }
html
<div class="page-header"> <img src="logo.png" height="40" /> <span>Invoice INV-2024-0142</span> </div>

The running() function lifts that element out of the document flow and into the page margin box, where it repeats on every page automatically. No JavaScript, no per-page loops.

Page numbers: "Page X of Y" the right way

counter(page) gives you the current page. counter(pages) gives you the total. Both are computed by the renderer after layout, so you don't need a two-pass approach.

css
@page { @bottom-right { content: "Page " counter(page) " of " counter(pages); font-family: system-ui, sans-serif; font-size: 10px; } }

If you want page numbers on every page except the cover, use named pages:

css
@page cover { @bottom-right { content: ""; } } .cover-page { page: cover; page-break-after: always; }

Controlling page breaks

Manual breaks: insert them where logical sections end.

css
.invoice-section { page-break-after: always; /* legacy */ break-after: page; /* modern */ }

Avoid breaking inside critical elements — table rows, signature blocks, summary boxes:

css
tr, .signature-block, .totals-summary { break-inside: avoid; }

For long tables, repeat the table header on every page:

css
thead { display: table-header-group; } tfoot { display: table-footer-group; }

This is browser-native behavior, and it just works in Chromium-based PDF engines.

Generating with Kamy

If you're using Kamy, headers, footers, and page numbering are part of the template config — you don't need to wire up CSS paged media yourself unless you want to.

ts
import { kamy } from "@kamydev/sdk"; const pdf = await kamy.documents.render({ template: "invoice", data: { invoiceNumber: "INV-2024-0142", lineItems: longLineItemArray, }, options: { header: { template: "invoice-header", height: 80, }, footer: { template: "invoice-footer", height: 60, showPageNumbers: true, pageNumberFormat: "Page {page} of {pages}", }, }, });

The header and footer templates are rendered per page with {page} and {pages} available as variables. Body content automatically reserves space based on the declared heights.

A pre-flight checklist

Before shipping a multi-page PDF generator to production, verify each of these against a realistic dataset that spans at least 5 pages:

txt
[ ] Header appears on every page, including the last [ ] Footer appears on every page, including the first [ ] Page numbers increment correctly and show correct total [ ] No content overlaps the header or footer [ ] Tables repeat their header row on each new page [ ] No orphaned rows (single row of a group on its own page) [ ] Page breaks don't split signature blocks or totals [ ] Margins are consistent on every page [ ] Cover page (if any) has no header/footer [ ] Fonts render the same on page 1 and page N

A note on testing

Always test with three input sizes: minimal (1 page), typical (3–5 pages), and stress (50+ pages). Most layout bugs only appear on page boundaries, and bugs only appear after the second or third break. A single 12-page snapshot in your CI is wor