Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
When a PDF from dynamic HTML has missing backgrounds, unexpected colors, repeated-looking elements, or awkward page breaks, first make the browser’s inputs predictable: choose print or screen media deliberately, fix the paper geometry, wait for the page’s data and assets, then adjust print pagination CSS. Puppeteer generates PDFs using print CSS by default; its PDF options and the page’s readiness state can change the result as much as the HTML itself.
Why dynamic HTML looks different in a PDF
A browser’s PDF is not automatically a picture of the page as it appears in a tab. Puppeteer documents that its PDF generation uses the print CSS media type. Playwright documents the same default, and provides page.emulateMedia() to switch media. A stylesheet can therefore hide, resize, recolor, or rearrange content for printing even when the screen version looks correct.
There are four inputs to pin down before changing CSS:
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Clear out junk files and repair common Windows errorsFree Scan →- Media: print styles or screen styles.
- Paper geometry: paper size, margins, scale, and whether CSS or API options control the page size.
- Readiness: whether application data, images, stylesheets, and fonts have finished loading.
- Pagination: where content is allowed to split and where sections should start.
If any of these varies between runs, the same document can wrap differently or place page elements differently. Fix them first; then a visual pattern is easier to diagnose rather than chasing one symptom at a time.
#1 Best Overall
Build a stable Puppeteer baseline
Set a deliberate print stylesheet and use a fixed PDF configuration. This example assumes the page exposes an application-specific window.__PDF_READY__ flag only after it has finished rendering its data. Replace the URL and readiness condition with those for your application.
import puppeteer from 'puppeteer';
const url = 'https://example.com/report';
const outputPath = 'report.pdf';
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 900 });
await page.goto(url, { waitUntil: 'domcontentloaded' });
// Use a signal owned by the application, not an arbitrary sleep.
await page.waitForFunction(() => window.__PDF_READY__ === true, {
timeout: 30000,
});
// Wait for images already present in the document to finish decoding.
await page.evaluate(async () => {
await Promise.all(
Array.from(document.images, (img) => {
if (img.complete) return Promise.resolve();
return new Promise((resolve) => {
img.addEventListener('load', resolve, { once: true });
img.addEventListener('error', resolve, { once: true });
});
}),
);
if (document.fonts) await document.fonts.ready;
});
await page.pdf({
path: outputPath,
printBackground: true,
preferCSSPageSize: true,
});
} finally {
await browser.close();
}
The example uses a 30-second application-ready timeout as a code setting, not as a claim that every page needs that long. Choose a limit appropriate to your service and report a useful error if it is exceeded. Puppeteer’s guide says page.pdf() waits for fonts by default; the explicit font wait above makes the readiness intent visible, while the application flag remains necessary for data and other asynchronous rendering.
The image wait resolves on both load and error so one broken image does not hang the capture. If image failures should fail the PDF job, change that handler to reject and surface the failing asset. Also account for images your application inserts after the readiness signal: the signal should be set after that work is complete, or the wait condition should include it.
Choose screen or print media intentionally
Use print media for documents
For invoices, reports, or other document-like output, keep print media active and provide a dedicated @media print stylesheet. Remove screen-only navigation and controls, set readable type sizes, and define print-specific layout and page breaks there. The browser’s default print behavior is not a guarantee that your screen styles are suitable for paper.
Rank #2
Use screen media when the PDF must match the screen design
If the desired output is the screen composition, switch media before PDF generation:
await page.emulateMediaType('screen');
await page.pdf({ path: 'screen-layout.pdf', printBackground: true });
This changes which media rules apply; it does not freeze the page’s viewport, wait for application data, or guarantee identical pagination across different paper settings. Treat the result as a screen-styled layout rendered onto PDF pages, then inspect the actual page boundaries.
Preserve backgrounds, colors, and page geometry
Enable background printing when the design needs it
By default, print output may omit background graphics. Set printBackground: true when the PDF needs background colors or images. For designs where exact color reproduction matters, add -webkit-print-color-adjust: exact to the relevant print rules. Use it deliberately: a background that is decorative on screen may reduce readability or consume unnecessary ink when printed.
@media print {
html {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
.report-header {
background: #17324d;
color: #fff;
}
}
Make one source control the paper size
CSS @page can define paper dimensions and margins. Set preferCSSPageSize: true when that CSS geometry should take priority over the API paper settings:
Rank #3
@page {
size: A4;
margin: 16mm 14mm 18mm;
}
Choose one authority while debugging. With CSS preferred, keep the CSS @page declaration authoritative. Otherwise, use Puppeteer’s format, or its width and height, plus margin and scale options as appropriate. Competing CSS and API geometry makes it harder to explain a change in wrapping, whitespace, or where a design element appears on successive pages. Temporarily remove competing settings to isolate a geometry problem, then restore only the configuration you actually need.
Lock the viewport as well as the PDF paper settings. A viewport-dependent layout can choose different columns or element widths before the browser paginates it. Keep the viewport, paper dimensions, margins, and scale identical when comparing two output files.
Wait for dynamic content before calling page.pdf()
A navigation event alone does not mean an application has finished rendering. Client-side data requests, delayed component updates, image loading, and font loading can all affect the final layout. Puppeteer’s official guide demonstrates launching the browser, creating a page, navigating with an explicit wait condition, calling page.pdf(), and closing the browser. Adapt that lifecycle to the readiness guarantees your application can make.
- Prefer an application signal. For example, set a flag after the report data is rendered and any layout-affecting updates have settled.
- Wait for essential selectors. If a known report container is the reliable signal, wait for it to appear and, where needed, for its content to be populated.
- Use network-idle waits with care. Long polling, analytics, or other ongoing requests can prevent a page from becoming idle. Conversely, network idle alone does not prove that client-side rendering is finished.
- Set timeouts and surface failures. A timeout should identify which readiness condition failed; it should not silently produce a partial document.
A short fixed delay is a weak substitute for a readiness condition: fast runs waste time and slow runs can still capture too early. If your application cannot expose a single completion flag, wait for the specific data and assets that affect the printed result.
Rank #4
Control where content breaks across pages
Once media, timing, and geometry are fixed, use print pagination rules to handle splits. Apply them to the smallest meaningful block rather than to an entire long document.
@media print {
.card,
.summary-block {
break-inside: avoid;
}
.chapter {
break-before: page;
}
.chapter:first-child {
break-before: auto;
}
}
break-inside: avoidasks the browser to keep a block together when it can fit on a page. An item taller than a full printable page cannot be kept intact; inspect oversized cards, tables, and images separately.break-before: pagecreates a deliberate section start. Use it for true document boundaries, not every repeated component.break-aftercan similarly control what follows a section. Add it only where an intentional boundary is needed.
For recurring elements such as headers or footers, define the intended behavior through @page where supported by the browser and test the result in the production browser version. Check every page boundary, not only the first page: content height and a single oversized block can change later pagination.
Diagnose in a repeatable order
- Freeze the reproduction. Use a fixed browser version, viewport, input data, paper format, margins, and scale. Save the generated PDF for comparison.
- Check the active media. Decide whether the intended output is print or screen. If it is screen, call
page.emulateMediaType('screen'); if it is print, keep print media and correct the print stylesheet. - Test backgrounds and colors. Enable
printBackground; add-webkit-print-color-adjust: exactonly for elements that need exact print colors. - Isolate page geometry. Decide whether CSS
@pageor API paper settings control the result. Remove competing width, height, format, margin, or scale values during the test. - Verify readiness. Confirm that application data and layout-affecting assets are ready before PDF generation. Check whether images failed, fonts loaded, or a late update changed element sizes.
- Adjust pagination. Add the necessary break rules to sections, tables, or cards, then inspect all page edges for clipped or stranded content.
- Compare libraries only after inputs match. Puppeteer and Playwright can be compared on media controls, PDF options, readiness behavior, and browser lifecycle; different timing or geometry makes the comparison inconclusive.
Troubleshoot common PDF symptoms
| Symptom | Likely cause | First fix to try |
|---|---|---|
| Background colors or images are missing | PDF background printing is disabled or print CSS removes the background. | Set printBackground: true and inspect the active @media print rules. |
| Colors look muted or differ from the design | Print color adjustment is not forcing the intended exact colors. | Apply -webkit-print-color-adjust: exact to the relevant print elements and retest. |
| Text wraps differently or content shifts between runs | Media, viewport, paper geometry, scale, or readiness is inconsistent. | Fix those inputs and wait for data and fonts before changing the layout CSS. |
| Cards, rows, or sections split awkwardly | The browser is paginating without suitable break rules, or a block is too large to fit. | Try break-inside: avoid on compact blocks; split or redesign oversized content. |
| A section starts on an unexpected page | A forced break, margin, or competing page-size setting changes available space. | Review break-before/break-after and establish one source of paper geometry. |
| PDF is missing late data or images | Capture starts after navigation but before application rendering has settled. | Wait for an app-specific ready signal and essential assets; make timeout failures visible. |
| The job hangs waiting for the page | A generic idle condition may never occur because requests continue in the background. | Use a narrower application signal or selector instead of waiting indefinitely for network idleness. |
Performance, reliability, and cost considerations
PDF output is sensitive to rendering inputs, so reproducibility comes from keeping the browser version, page state, viewport, and geometry controlled—not from adding more wait time indiscriminately. A readiness condition should wait only for work that can alter the document. This avoids both premature captures and unnecessary delay.
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteAlways close the browser in a finally block, as in the example, so navigation, readiness, or PDF errors do not leave the browser running. Log which stage failed and retain enough context to reproduce the input. If a document can legitimately exceed your configured readiness timeout, adjust the limit based on the application’s behavior rather than silently accepting incomplete output.
For a system that renders many documents, measure your own workload: document size, asset volume, browser lifecycle, and concurrency affect resource use. The cited API guidance does not establish a universal throughput, latency, or cost figure for a Node.js PDF job.
Or skip the browser setup
If your immediate goal is a clean visual capture of a webpage rather than a carefully paginated, selectable-text PDF, ScreenshotNeo is a screenshot API and MCP server. A single GET request can return a PNG, JPEG, WebP, or PDF; the Node.js example below shows a visual screenshot call. It is not a drop-in replacement for Puppeteer pagination CSS or a guarantee that a multi-page report will lay out as intended.
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
See the ScreenshotNeo API documentation for request options. ScreenshotNeo accepts cookie or consent banners as a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each of those steps can be turned off. Bot checks or CAPTCHAs, blank pages, failed loads, timeouts, and cache hits are not billed, and responses identify the page verdict and billing status in headers. Its MCP server offers take_screenshot, get_page_info, and capture_pdf tools for AI agents using Claude, Cursor, or another MCP client.
Free tools Windows power users keep installed
One-click scans. No signup required.
The free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots. Sign up for the free plan to try it without a card.
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.

