DEV Community

Philipp Staudt
Philipp Staudt

Posted on Originally published at formfeed.dev AI-assisted

Page breaks in tables: what Chromium honours in 2026

A table that fits on one page is easy. The trouble starts at the page break: a header that does not come back, a row cut through the middle, a total printed under the first thirty rows, a footer lying on top of the last line. Most advice about this was written for browsers that paginated differently, and gets copied forward.

So we measured. We rendered 21 small table documents through our render worker — the same headless Chromium, version 153.0.8010.12, that prints every Formfeed PDF — and read the finished PDFs back with pdf.js to see which page every row, header and total landed on. Every case below links to its PDF.

The starting point

Every Formfeed document starts with a short print reset. Four of its lines are about tables (in effect; the real rules sit in :where() so that any rule of the template wins):

table { break-inside: auto; border-collapse: collapse; }
tr { break-inside: avoid; }
thead { display: table-header-group; }
tfoot { display: table-footer-group; }
Enter fullscreen mode Exit fullscreen mode

The tr line is ours, not Chromium's: without it, Chromium splits a row wherever the page ends. The cases use A4 with 20 mm margins, ten-point text, and a page-number footer from Chromium's footer template unless they say otherwise. Rows are numbered, and a tall row's lines are numbered too (08·5 is the fifth line of row 8), so a split shows.

What Chromium keeps

Case CSS What the PDF shows
60 rows — the header row repeats at the top of page 2 PDF
rows six lines tall tr { break-inside: avoid } 7 whole rows per page, none split PDF
the same rows tr { break-inside: auto } rows 8, 16 and 24 split across pages PDF
one row of 90 lines tr { break-inside: avoid } the row starts a new page and then splits anyway; all 90 lines are printed PDF
groups of six rows, each its own tbody tbody { break-inside: avoid } page 1 ends after row 30, a group boundary, instead of row 32 PDF
a heading at the foot of a page h2 { break-after: avoid } the heading moves to the next page with its table PDF
the last row alone on a page tbody tr:last-child { break-before: avoid } row 8 goes along: 7 rows, then 2 PDF
the last two rows as their own tbody tbody.tail { break-inside: avoid } the same, 7 and 2 PDF
the table inside a flex, grid, overflow: auto, overflow: hidden or absolutely positioned wrapper — paginates like a bare table, header repeated flex · grid · auto · hidden · absolute

The last row surprised us most, because every one of those wrappers turns up in advice on why a table stopped paginating. In Chromium 153 none of them stops it.

The two keep-together rules are the ones worth having in every template. Without them, the heading stays behind on its own:

A section heading alone at the foot of page 1, its table starting on page 2

and the last row of a table ends up alone on a page of its own:

Rows 1 to 8 on page 1, row 9 alone under a repeated header on page 2

With break-before: avoid on the last row, Chromium takes the row before it along, so no page holds a single row:

Rows 1 to 7 on page 1, rows 8 and 9 on page 2

And this is what a tall row looks like when nothing asks Chromium to keep it together — the amount stays on page 1 and the last two lines of the description go to page 2:

Row 8 split across two pages, its lines 5 and 6 under the repeated header on page 2

Three ways to get it wrong

The total on every page

tfoot looks like the place for a total. But a tfoot is a footer group, and Chromium repeats it at the foot of every page the table reaches, just as it repeats thead at the top. The grand total turns up under row 31 on page 1:

The total 7,200.00 printed at the foot of page 1, after row 31 of 60

Two fixes. Put the total in a last tbody of its own. Or keep the tfoot and turn it into an ordinary row group, tfoot { display: table-row-group; } — then it prints once, on the last page, as long as it comes after the tbody in the markup. HTML also allows the tfoot before the tbody; with the override, such a total prints above the first row.

The repeat cannot carry a running subtotal either. What Chromium repeats is a copy of the same content, so a "carried forward" line that sums the rows above it on each page is not something a tfoot can do.

A row cut in half

Wrap the table in display: inline-block — a common way to centre it or shrink it to its content — and Chromium stops paginating the table inside. It prints the box across the page edge as it is: row 33 is cut through the middle, and page 2 has no header.

Row 33 sliced horizontally at the foot of page 1, its lower half at the top of page 2, no header on page 2

A block wrapper does not do this. thead { display: block; }, which some scrolling-table CSS sets, keeps the rows whole but loses the repeated header.

The row under the footer

A footer made with position: fixed; bottom: 0 does repeat on every page. The table does not know it is there. Row 32 slides underneath, and since the table thinks row 32 fitted, page 2 carries on with row 33 — row 32 is never printed whole:

A fixed footer covering the lower half of row 32; page 2 starts at row 33

The clean fix is not to put the footer in the page at all. Chromium's own footer template is printed in the bottom margin, where content never reaches; every other case in this post has one, and none lost a row to it. If the footer has to live in the page, the repeat that caused the first problem solves this one: an empty tfoot row as tall as the footer repeats on every page and keeps the rows clear of it.

<tfoot><tr class="reserve"><td colspan="3"></td></tr></tfoot>
Enter fullscreen mode Exit fullscreen mode
tr.reserve td { height: 10mm; padding: 0; border: 0; }
Enter fullscreen mode Exit fullscreen mode

Row 31 is the last on page 1, clear of the fixed footer; page 2 starts at row 32

The short version

  • Keep rows whole with tr { break-inside: avoid }, and groups of rows with a tbody each and break-inside: avoid.
  • Keep headings with their table: h2 { break-after: avoid }.
  • Keep the last row company: tbody tr:last-child { break-before: avoid }.
  • Put totals in a last tbody, or make the tfoot a table-row-group after the tbody.
  • No inline-block around a table, no display: block on its thead.
  • Footers go in the footer template, not in position: fixed.

In Formfeed

The print reset above is on by default and every line of it can be overridden. The Footer tab of the editor is Chromium's footer template, so a footer made there sits in the margin. The editor's paged preview is built to lay out like the PDF, and True render sends the template through the same worker these cases went through. The page-break section of the docs has the reset, and the report example is a two-page document with a chart and a table of monthly figures.

Rendered and measured on 21 September 2026 with Chromium 153.0.8010.12. Chromium's layout changes between versions; we re-run the cases when the worker's Chromium does.

Top comments (0)