Use print CSS, define the PDF page geometry, give the table a predictable width, and apply best-effort fragmentation rules to rows and cells. In Puppeteer, combine those rules with page.emulateMediaType('print') (the default for page.pdf()), @page, table-layout: fixed, and a semantic <thead>. Then inspect the actual PDF: CSS cannot keep an element together when it is taller than a page, and repeated-header behavior varies between PDF engines.
Why an HTML table that looks fine on screen breaks in a PDF
A browser screen is a continuous canvas. A PDF is a sequence of fixed-size pages, so the renderer must fragment content at page boundaries. The print media stylesheet, available paper width, font metrics, row height, and renderer implementation all affect the result.
Typical symptoms have different causes:
- A row is divided between pages because the renderer is allowed to fragment it, or because the row is taller than the printable page.
- The header appears only on page one because the table has no semantic
<thead>, or the selected engine does not repeat header groups. - Text or columns are clipped because the table’s minimum width exceeds the printable width.
- Pages are blank or shifted because a forced break, fixed-height container, oversized margin, or overlong unbreakable element consumes the page.
Treat the PDF as a separate layout target. Keep screen rules and print rules explicit, generate with the same browser and fonts used in production, and validate the resulting file rather than relying only on a print preview.
Set the page size and margins first
Page geometry determines how much width and height the table actually has. Use CSS @page for a document-level default, then keep the PDF API settings consistent with it.
Recommended Free Tools
#1 Best Overall
A4 and Letter examples
| Target | CSS declaration | When to use it |
|---|---|---|
| A4 portrait | @page { size: A4 portrait; margin: 12mm; } |
Most international reports and invoices |
| Letter portrait | @page { size: Letter portrait; margin: 0.5in; } |
Documents intended for US Letter paper |
| A4 landscape | @page { size: A4 landscape; margin: 10mm; } |
Wide tables with many columns |
In Puppeteer, format, width, height, landscape, and margin are renderer options. preferCSSPageSize: true gives the CSS @page rule precedence over the API’s format, width, and height. Choose one source of truth where possible; conflicting values can cause unexpected scaling.
Use a print stylesheet that controls width and fragmentation
This pattern is a practical baseline for a report table. It uses a fixed layout, semantic headers, wrapping for long tokens, and both modern and legacy fragmentation properties.
@page {
size: A4 portrait;
margin: 12mm;
}
@media print {
table.report {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
}
table.report thead {
display: table-header-group;
}
table.report tr,
table.report tbody,
table.report td,
table.report th {
break-inside: avoid;
page-break-inside: avoid;
}
table.report th,
table.report td {
padding: 3pt 4pt;
overflow-wrap: anywhere;
vertical-align: top;
}
}
break-inside: avoid is a request, not an absolute command. If a row is taller than the printable page, the renderer must print part of it and continue it. Avoid creating such rows by shortening prose, allowing sensible wrapping, or splitting a very large record into multiple rows.
Make column widths predictable
With table-layout: fixed, the table uses the available width instead of expanding indefinitely to fit intrinsic content. Add a <colgroup> when certain columns need stable proportions.
Free tools Windows power users keep installed
One-click scans. No signup required.
<table class='report'>
<colgroup>
<col style='width: 16%'>
<col style='width: 44%'>
<col style='width: 20%'>
<col style='width: 20%'>
</colgroup>
<thead>
<tr><th>ID</th><th>Description</th><th>Owner</th><th>Status</th></tr>
</thead>
<tbody>...</tbody>
</table>
Use overflow-wrap: anywhere for URLs, hashes, SKU codes, and other tokens with no natural spaces. If the table is still wider than the page, use landscape orientation, reduce cell padding and font size, or redesign the table. Shrinking everything until it is unreadable is not a successful fit.
Do not hide overflow blindly
overflow: hidden may conceal text and make the PDF appear correct while silently losing data. Prefer wrapping, a wider page, or a deliberate column redesign. Fixed pixel widths are especially risky because the printable width changes with paper size and margins.
Repeat the header on every page
Put column labels in <thead>, not in the first body row. In print CSS, display: table-header-group is a widely used technique that asks the renderer to repeat that group when the table continues on a new page.
<table class='report'>
<thead>
<tr>
<th scope='col'>Invoice</th>
<th scope='col'>Description</th>
<th scope='col'>Amount</th>
</tr>
</thead>
<tbody>...</tbody>
</table>
Header repetition is renderer-dependent. Test the exact browser or conversion engine used in deployment. If a legacy engine ignores the rule, the reliable fallback is to split the data into separate tables and place a header before each one; that changes the document structure but makes the repeated labels explicit.
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Generate the PDF with Puppeteer
Puppeteer’s page.pdf() generates a PDF using the print CSS media type by default. Calling emulateMediaType('print') makes that choice explicit. The following complete example writes an A4 PDF, preserves CSS page sizing, prints backgrounds, and waits for network activity to settle.
import puppeteer from 'puppeteer';
const html = `
<!doctype html>
<html>
<head>
<meta charset='utf-8'>
<style>
@page { size: A4 portrait; margin: 12mm; }
@media print {
table.report { width: 100%; table-layout: fixed; border-collapse: collapse; }
table.report thead { display: table-header-group; }
table.report tr, table.report tbody, table.report td, table.report th {
break-inside: avoid;
page-break-inside: avoid;
}
th, td { padding: 3pt 4pt; overflow-wrap: anywhere; vertical-align: top; }
th { background: #eeeeee; }
}
</style>
</head>
<body>
<h1>Sales report</h1>
<table class='report'>
<thead><tr><th>Item</th><th>Details</th><th>Qty</th><th>Total</th></tr></thead>
<tbody>
<tr><td>A-100</td><td>Example product</td><td>4</td><td>$80</td></tr>
<tr><td>B-200</td><td>A description long enough to demonstrate wrapping in a fixed column.</td><td>2</td><td>$40</td></tr>
</tbody>
</table>
</body>
</html>`;
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle0' });
await page.emulateMediaType('print');
await page.pdf({
path: 'report.pdf',
format: 'A4',
printBackground: true,
preferCSSPageSize: true,
margin: { top: '12mm', right: '12mm', bottom: '12mm', left: '12mm' }
});
} finally {
await browser.close();
}
If you need screen styling instead, call page.emulateMediaType('screen') before page.pdf(). That is an intentional exception; otherwise keep the print stylesheet as the source for PDF layout.
Handle wide tables and long documents deliberately
Choose orientation before reducing type
Landscape gives each row more horizontal space and often produces a more readable result than a tiny portrait table. Set landscape: true in the renderer or use @page { size: A4 landscape; }; keep the two settings aligned.
Break a report into logical tables
A single table containing thousands of records can be difficult to debug and expensive to render. Separate sections by month, customer, or category when each section has a meaningful heading. Give each table its own <thead> so labels remain available after a section break.
Control typography
Use a font that is installed or bundled in the rendering environment. A missing font changes glyph widths and line wrapping, which can move a row onto another page. Avoid relying on a locally installed desktop font when PDFs are generated in containers or CI.
What to compare when choosing a PDF engine
Different engines implement print CSS and fragmentation differently. Evaluate the actual candidates against the same fixture document.
| Comparison axis | Why it affects tables | Validation method |
|---|---|---|
| Print-media fidelity | Determines whether @media print rules are honored. |
Compare computed styles and the rendered PDF. |
| Row fragmentation and header groups | Controls whether rows stay together and headers repeat. | Use rows near a page boundary and inspect every page. |
| Page-size and margin controls | Defines the printable width and height. | Test A4, Letter, portrait, and landscape. |
| Font availability | Changes line breaks and therefore pagination. | Run in the same image or host as production. |
| JavaScript execution | Some reports need data loading or layout scripts before capture. | Wait for the required selector or network state. |
| Operational cost and consistency | Browser startup, concurrency, and version drift affect throughput. | Pin the engine version and measure representative documents. |
Troubleshooting: symptom, cause, and fix
Rows split unexpectedly
- Apply both
break-inside: avoidandpage-break-inside: avoidto the row, and if necessary to its cells or row group. - Check whether the row is taller than one printable page. No CSS rule can fit an oversized element into a smaller page.
- Reduce excessive padding, shorten repeated prose, or split the record into multiple rows.
Columns are clipped or extend beyond the page
- Confirm the table has
width: 100%andtable-layout: fixed. - Add explicit column percentages with
<colgroup>. - Wrap long tokens with
overflow-wrap: anywhere. - Switch to landscape or redesign the columns instead of hiding overflow.
The header does not repeat
- Move labels into a real
<thead>. - Apply
display: table-header-groupin print CSS. - Verify the chosen renderer supports repeated header groups; otherwise split the document into multiple tables.
The PDF has blank or oddly shifted pages
- Inspect
break-before,break-after, and legacypage-break-*rules for accidental forced breaks. - Remove fixed-height wrappers and check for containers taller than the page.
- Reduce oversized margins and confirm that CSS and API page sizes are not fighting each other.
The PDF differs from browser preview
- Use the same renderer version, print media mode, fonts, and page-size settings in both paths.
- Wait for web fonts, images, and data-driven rows before calling
page.pdf(). - Capture a deterministic fixture page in automated tests so pagination changes are visible during upgrades.
Performance and reliability practices
Reuse a browser process when generating many files, but isolate pages and close them after each job. Limit concurrency to what the host can render without memory pressure. Cache or preload fonts and static assets, and avoid layout scripts that repeatedly force synchronous reflow.
Rank #4
For reliable pagination, keep the input deterministic: freeze timestamps, use stable data ordering, wait for a known completion selector, and pin the browser version. Compare PDFs after upgrades because a font or Chromium change can legitimately alter line wrapping even when the HTML is unchanged.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Production checklist
- Choose A4, Letter, or another target and set explicit margins.
- Write print-specific CSS and generate with print media.
- Set the table to
width: 100%; use fixed layout and column widths where needed. - Use semantic
<thead>and test repeated headers in the chosen engine. - Apply both modern and legacy row-fragmentation properties.
- Wrap long tokens; never rely on hidden overflow to conceal content.
- Use landscape or redesign genuinely wide tables.
- Ensure fonts, images, and dynamic data are loaded before capture.
- Render a boundary-case fixture and inspect the actual PDF on every engine or version change.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server for developers. It can capture a page as PNG, JPEG, WebP, or PDF, with PDF paper size, margins, landscape mode, and page ranges available as options. Before capture it accepts cookie or consent banners as a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be switched off. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
For a quick one-call capture, see the ScreenshotNeo documentation:
curl -G 'https://api.screenshotneo.com/v1/shot' -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/report -o shot.webp
import requests
r = requests.get('https://api.screenshotneo.com/v1/shot', params={'access_key': 'YOUR_API_KEY', 'url': 'https://example.com/report'}, timeout=90)
open('shot.webp', 'wb').write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com/report' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Use the PDF capture options in the documentation when the deliverable must be a PDF rather than an image. ScreenshotNeo has a free tier of 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots, and every feature is included on every plan. Create a free ScreenshotNeo account.
Frequently Asked Questions
Should I put a page break inside the table or between tables?
Prefer a break between logical tables or sections when a heading and its rows belong together. Use forced breaks sparingly; an unnecessary break can create a mostly blank page and is harder to maintain than a natural flow.
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Scan for outdated or missing drivers - takes under a minute3Repair Windows errors before they cause bigger problemsHow can I make a PDF table accessible?
Use real table semantics: a caption when context is needed, one header row in <thead>, and scope='col' or scope='row' where appropriate. Keep the visual print layout separate from the document’s semantic structure.
Why did changing a font alter the number of pages?
Font metrics change glyph widths and line wrapping. Different wrapping changes row heights, so page boundaries move even when the table data and CSS are unchanged.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

