We shipped a report that looked correct in the browser and was wrong on paper — in a way nobody could see until it came out of a printer.
The table ended with a total row, marked up the way the element names suggest:
<table>
<thead>
<tr><th>Item</th><th>Count</th></tr>
</thead>
<tbody>
<!-- 200 rows -->
</tbody>
<tfoot>
<tr><th>Total</th><td>31,432</td></tr>
</tfoot>
</table>
On screen: one total, at the bottom, correct.
Printed across five pages: the total row appeared at the bottom of every page. Same number each time. A reader flipping through sees what looks like a running subtotal that never changes, then a final page whose “total” is indistinguishable from the four before it.
This is specified behaviour, not a bug
<thead> and <tfoot> are repeating elements. When a table breaks across
pages, the header repeats at the top of each fragment and the footer repeats
at the bottom of each. That is the point of them — for a long table you want
the column labels on page four.
Which makes <tfoot> exactly the wrong element for a grand total. It is for
content that is true of every fragment, like a units label. A number that is
true only of the whole table does not belong there.
The fix
Put the total in the last <tbody> row and style it:
<tbody>
<!-- 200 rows -->
<tr class="total">
<th>Total</th><td>31,432</td>
</tr>
</tbody>
.total { border-top: 2px solid; font-weight: 600; }
It now appears once, at the end, wherever that falls.
The corollary is the useful part: anything a continuation page must carry
belongs in <thead>. Column headers, units, the “as of” date, the site
name. If a reader picking up page three in isolation needs it, it goes in
the head. If it describes the whole document, it goes in the flow.
The part that actually cost us the time
We had looked at this report many times. In a browser it is right. The bug exists only in paginated output, and pagination does not happen until something paginates.
So we stopped eyeballing and started rendering. Drive the page through a real print pipeline and read the text back out:
const pdf = await page.printToPDF({ printBackground: true });
$ pdftotext report.pdf - | grep -c "Total"
5
Five. Expected one. That grep is the whole test, it runs in CI, and it would have caught this before anyone printed anything.
Print bugs are only visible in rendered pages. If your software produces something people put on paper, something in your pipeline has to actually put it on paper.