<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Philipp Staudt</title>
    <description>The latest articles on DEV Community by Philipp Staudt (@staudtit).</description>
    <link>https://dev.to/staudtit</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4151750%2F45962f8b-b2d8-499d-ba65-ca2fa51297d4.png</url>
      <title>DEV Community: Philipp Staudt</title>
      <link>https://dev.to/staudtit</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/staudtit"/>
    <language>en</language>
    <item>
      <title>Page breaks in tables: what Chromium honours in 2026</title>
      <dc:creator>Philipp Staudt</dc:creator>
      <pubDate>Wed, 30 Sep 2026 08:34:24 +0000</pubDate>
      <link>https://dev.to/staudtit/page-breaks-in-tables-what-chromium-honours-in-2026-258g</link>
      <guid>https://dev.to/staudtit/page-breaks-in-tables-what-chromium-honours-in-2026-258g</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The starting point
&lt;/h2&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;table&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;break-inside&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;border-collapse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;collapse&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;tr&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;break-inside&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;avoid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;thead&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;table-header-group&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;tfoot&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;table-footer-group&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;tr&lt;/code&gt; 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 (&lt;code&gt;08·5&lt;/code&gt; is the fifth line of row 8), so a split shows.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Chromium keeps
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Case&lt;/th&gt;
&lt;th&gt;CSS&lt;/th&gt;
&lt;th&gt;What the PDF shows&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;60 rows&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;the header row repeats at the top of page 2&lt;/td&gt;
&lt;td&gt;&lt;a href="https://formfeed.dev/blog/page-breaks-in-tables/long-table.pdf" rel="noopener noreferrer"&gt;PDF&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rows six lines tall&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tr { break-inside: avoid }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;7 whole rows per page, none split&lt;/td&gt;
&lt;td&gt;&lt;a href="https://formfeed.dev/blog/page-breaks-in-tables/tall-rows.pdf" rel="noopener noreferrer"&gt;PDF&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;the same rows&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tr { break-inside: auto }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;rows 8, 16 and 24 split across pages&lt;/td&gt;
&lt;td&gt;&lt;a href="https://formfeed.dev/blog/page-breaks-in-tables/tall-rows-auto.pdf" rel="noopener noreferrer"&gt;PDF&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;one row of 90 lines&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tr { break-inside: avoid }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the row starts a new page and then splits anyway; all 90 lines are printed&lt;/td&gt;
&lt;td&gt;&lt;a href="https://formfeed.dev/blog/page-breaks-in-tables/row-taller-than-page.pdf" rel="noopener noreferrer"&gt;PDF&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;groups of six rows, each its own &lt;code&gt;tbody&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tbody { break-inside: avoid }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;page 1 ends after row 30, a group boundary, instead of row 32&lt;/td&gt;
&lt;td&gt;&lt;a href="https://formfeed.dev/blog/page-breaks-in-tables/tbody-groups.pdf" rel="noopener noreferrer"&gt;PDF&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a heading at the foot of a page&lt;/td&gt;
&lt;td&gt;&lt;code&gt;h2 { break-after: avoid }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the heading moves to the next page with its table&lt;/td&gt;
&lt;td&gt;&lt;a href="https://formfeed.dev/blog/page-breaks-in-tables/heading-kept.pdf" rel="noopener noreferrer"&gt;PDF&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;the last row alone on a page&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tbody tr:last-child { break-before: avoid }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;row 8 goes along: 7 rows, then 2&lt;/td&gt;
&lt;td&gt;&lt;a href="https://formfeed.dev/blog/page-breaks-in-tables/last-row-kept.pdf" rel="noopener noreferrer"&gt;PDF&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;the last two rows as their own &lt;code&gt;tbody&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tbody.tail { break-inside: avoid }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the same, 7 and 2&lt;/td&gt;
&lt;td&gt;&lt;a href="https://formfeed.dev/blog/page-breaks-in-tables/last-rows-kept-tbody.pdf" rel="noopener noreferrer"&gt;PDF&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;the table inside a flex, grid, &lt;code&gt;overflow: auto&lt;/code&gt;, &lt;code&gt;overflow: hidden&lt;/code&gt; or absolutely positioned wrapper&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;paginates like a bare table, header repeated&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://formfeed.dev/blog/page-breaks-in-tables/flex-wrapper.pdf" rel="noopener noreferrer"&gt;flex&lt;/a&gt; · &lt;a href="https://formfeed.dev/blog/page-breaks-in-tables/grid-wrapper.pdf" rel="noopener noreferrer"&gt;grid&lt;/a&gt; · &lt;a href="https://formfeed.dev/blog/page-breaks-in-tables/overflow-auto-wrapper.pdf" rel="noopener noreferrer"&gt;auto&lt;/a&gt; · &lt;a href="https://formfeed.dev/blog/page-breaks-in-tables/overflow-hidden-wrapper.pdf" rel="noopener noreferrer"&gt;hidden&lt;/a&gt; · &lt;a href="https://formfeed.dev/blog/page-breaks-in-tables/absolute-wrapper.pdf" rel="noopener noreferrer"&gt;absolute&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;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.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fasfs3hq1mg8aketznjw5.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fasfs3hq1mg8aketznjw5.webp" alt="A section heading alone at the foot of page 1, its table starting on page 2" width="800" height="704"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;and the last row of a table ends up alone on a page of its own:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr1ekltiq0i6khvtysgun.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr1ekltiq0i6khvtysgun.webp" alt="Rows 1 to 8 on page 1, row 9 alone under a repeated header on page 2" width="800" height="704"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;break-before: avoid&lt;/code&gt; on the last row, Chromium takes the row before it along, so no page holds a single row:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fld7nk6d5zgdfjxgt3kg5.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fld7nk6d5zgdfjxgt3kg5.webp" alt="Rows 1 to 7 on page 1, rows 8 and 9 on page 2" width="800" height="704"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnhc3mmewhxlg9lwnsv8m.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnhc3mmewhxlg9lwnsv8m.webp" alt="Row 8 split across two pages, its lines 5 and 6 under the repeated header on page 2" width="800" height="704"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways to get it wrong
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The total on every page
&lt;/h3&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh5hl9efdjw12uufl6x39.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh5hl9efdjw12uufl6x39.webp" alt="The total 7,200.00 printed at the foot of page 1, after row 31 of 60" width="800" height="704"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two fixes. Put the total in a last &lt;code&gt;tbody&lt;/code&gt; of its own. Or keep the &lt;code&gt;tfoot&lt;/code&gt; and turn it into an ordinary row group, &lt;code&gt;tfoot { display: table-row-group; }&lt;/code&gt; — then it prints once, on the last page, &lt;a href="https://formfeed.dev/blog/page-breaks-in-tables/tfoot-once.pdf" rel="noopener noreferrer"&gt;as long as it comes after the &lt;code&gt;tbody&lt;/code&gt; in the markup&lt;/a&gt;. HTML also allows the &lt;code&gt;tfoot&lt;/code&gt; before the &lt;code&gt;tbody&lt;/code&gt;; with the override, such a total &lt;a href="https://formfeed.dev/blog/page-breaks-in-tables/tfoot-first-once.pdf" rel="noopener noreferrer"&gt;prints above the first row&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;tfoot&lt;/code&gt; can do.&lt;/p&gt;

&lt;h3&gt;
  
  
  A row cut in half
&lt;/h3&gt;

&lt;p&gt;Wrap the table in &lt;code&gt;display: inline-block&lt;/code&gt; — 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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffcbbqp6oqhumsi9nxjm2.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffcbbqp6oqhumsi9nxjm2.webp" alt="Row 33 sliced horizontally at the foot of page 1, its lower half at the top of page 2, no header on page 2" width="800" height="704"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A block wrapper does not do this. &lt;code&gt;thead { display: block; }&lt;/code&gt;, which some scrolling-table CSS sets, keeps the rows whole but &lt;a href="https://formfeed.dev/blog/page-breaks-in-tables/thead-display-block.pdf" rel="noopener noreferrer"&gt;loses the repeated header&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The row under the footer
&lt;/h3&gt;

&lt;p&gt;A footer made with &lt;code&gt;position: fixed; bottom: 0&lt;/code&gt; 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:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh61r87z8u87gt0q0raig.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh61r87z8u87gt0q0raig.webp" alt="A fixed footer covering the lower half of row 32; page 2 starts at row 33" width="800" height="704"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;tfoot&lt;/code&gt; row as tall as the footer repeats on every page and keeps the rows clear of it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;tfoot&amp;gt;&amp;lt;tr&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"reserve"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;td&lt;/span&gt; &lt;span class="na"&gt;colspan=&lt;/span&gt;&lt;span class="s"&gt;"3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;&amp;lt;/tfoot&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;tr&lt;/span&gt;&lt;span class="nc"&gt;.reserve&lt;/span&gt; &lt;span class="nt"&gt;td&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10mm&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdb82wp73sqab558tsiwc.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdb82wp73sqab558tsiwc.webp" alt="Row 31 is the last on page 1, clear of the fixed footer; page 2 starts at row 32" width="800" height="704"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

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

&lt;h2&gt;
  
  
  In Formfeed
&lt;/h2&gt;

&lt;p&gt;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 &lt;strong&gt;True render&lt;/strong&gt; sends the template through the same worker these cases went through. The &lt;a href="https://docs.formfeed.dev/templates/writing-templates#page-breaks" rel="noopener noreferrer"&gt;page-break section of the docs&lt;/a&gt; has the reset, and the &lt;a href="https://formfeed.dev/examples/report?utm_source=devto&amp;amp;utm_content=page-breaks-in-tables" rel="noopener noreferrer"&gt;report example&lt;/a&gt; is a two-page document with a chart and a table of monthly figures.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;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.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>pdf</category>
      <category>css</category>
      <category>html</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
