Plainwork

Plainwork / Foliokit / HTML to PDF page breaks in Chrome: five patterns

HTML to PDF page breaks in Chrome: five patterns that work

If you print invoices, statements or reports from HTML, the same four problems come back: the table header does not repeat on page 2, a row is cut in half, the totals sit alone on the last page, and there is no "Page 2 of 5". These are the CSS patterns that fix them in Chrome and Chromium, each checked by printing a real PDF and reading the text back out of it (Chromium 141, 13 October 2026), plus the three things that do not work.

1. Repeat the table header on every page

thead { display: table-header-group; }
tr    { break-inside: avoid; }

We printed a table with 120 rows. It ran to three pages; page 2 started with the header row and the next data row, and no row was split. Without the first rule, the header appears once. The second rule keeps each row whole, which matters when a row has two lines (a description and a small reference under it).

2. Keep a totals block together

.totals, .notes { break-inside: avoid; }

The whole block moves to the next page if it does not fit, which can leave white space at the foot of the previous page. That is better than a total that is separated from its labels. It does not stop a block from being the only thing on a page; for that you need to shorten the content or move a row with it.

3. Page numbers and running text: margin boxes

@page {
  size: A4;
  margin: 18mm 16mm 22mm;
  @bottom-right { content: "Page " counter(page) " of " counter(pages);
                  font: 8pt Arial, sans-serif; }
  @bottom-left  { content: "Northfield Studio"; font: 8pt Arial, sans-serif; }
}

CSS page margin boxes arrived in Chrome 131 (release notes, November 2024). On Chromium 141 the footer printed "Page 2 of 3" on page 2. Older Chrome prints no footer, so check the version first (chrome --version). The same box works for @top-left, @top-right and the other fourteen positions. If you print from the command line, add --no-pdf-header-footer so Chrome's own date and URL line does not appear on top of yours.

4. A different first page, and a cover without a footer

@page :first { @top-right { content: none; } }
.cover { page: cover; break-after: page; }
@page cover { @bottom-right { content: none; } }

@page :first lets the first page leave out the running header. A named page (page: cover) lets a cover page have no footer at all. Both printed as expected on Chromium 141; the page count still includes the cover.

5. Use real text and tag the PDF

Chrome's command line makes a tagged PDF (pdfinfo reports Tagged: yes) with selectable text. Playwright's page.pdf() only does so when you ask: pass tagged: true and, if the document has headings, outline: true. Check the result with pdftotext -layout file.pdf -; if an amount is missing from that output, a screen reader will not find it either.

What does not work in Chrome

  • Page numbers in a table of contents (target-counter()). We printed a page that used it on Chromium 141 and the numbers were absent. Link the entries and leave the numbers out.
  • A chapter title in the running header (string-set with string()). The whole margin-box rule was dropped. Write one fixed title into the margin box, or print one PDF per chapter.
  • Grayscale with a filter. html { filter: grayscale(1) } looked right but turned the page into images: the PDF had no fonts and no selectable text.

If you are replacing wkhtmltopdf

wkhtmltopdf prints with screen CSS and Chrome prints with print CSS, so rules under @media print start to apply. The free translator turns a wkhtmltopdf command into the Chrome equivalent. Foliokit is seven finished templates (invoice, quote, receipt, statement, letter, certificate and Markdown report) that use all of the patterns above, with a command that fills them from JSON and prints with the Chrome you already have.

More from the catalogue

Other small tools

See everything →