Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • 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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
@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:

@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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • 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.

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: avoid asks 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: page creates a deliberate section start. Use it for true document boundaries, not every repeated component.
  • break-after can 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

  1. Freeze the reproduction. Use a fixed browser version, viewport, input data, paper format, margins, and scale. Save the generated PDF for comparison.
  2. 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.
  3. Test backgrounds and colors. Enable printBackground; add -webkit-print-color-adjust: exact only for elements that need exact print colors.
  4. Isolate page geometry. Decide whether CSS @page or API paper settings control the result. Remove competing width, height, format, margin, or scale values during the test.
  5. 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.
  6. Adjust pagination. Add the necessary break rules to sections, tables, or cards, then inspect all page edges for clipped or stranded content.
  7. 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.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Always 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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.