The journal · 14 July 2026 · 9 min read
Why your HTML to PDF output does not match the browser
Printing is not screenshotting. The renderer swaps your stylesheet, drops your backgrounds, paginates a layout built to scroll, and often fires before your content lands.
The short answer: your PDF does not match the browser because printing is not screenshotting. The renderer applies print stylesheets instead of screen ones, drops background colors by default, paginates a layout that was designed to scroll, and very often writes the file before your fonts, images or JavaScript have finished. Fix it in that order: print CSS, backgrounds, page geometry, then timing. Timing is the one that bites hardest.
Below are the seven failures that account for nearly every "the PDF looks wrong" bug, what causes each one, and the specific fix. They apply whether you are using Puppeteer, Playwright, wkhtmltopdf or a rendering API, because they are all doing the same underlying thing.
1. The page prints before your content arrives
This is the big one. If your page fetches data, draws a chart, lazy loads images or hydrates a framework, the renderer may reach "the page is loaded" long before the page is finished. You get a PDF with an empty chart box, a skeleton loader frozen mid animation, or a table with a spinner in it.
The trap is that waitUntil: 'networkidle0' feels like it should solve this and often does not. Network idle means no requests are in flight, which says nothing about whether your charting library has painted. A page that finished fetching but has not yet rendered is network idle.
The fix: wait for a specific element that only exists once the content is genuinely there. Name the selector explicitly. Every serious renderer supports this, whether as page.waitForSelector() in Puppeteer or a wait_for parameter in an API. Pick the element a human would look at to know the page is done, usually the total, the chart canvas or the last row of the table.
Lazy loaded images are a special case of this, and they produce a distinctive signature: the top of the document looks right and everything below the fold is blank, because a headless browser has a viewport but never scrolls. The images not loading guide covers that pattern and the one-line layout change that fixes it.
2. Backgrounds and colors disappear
Browsers deliberately strip background colors and images when printing, to save ink on someone's home printer. Your shaded table headers, colored callouts and dark hero print white, and the document looks broken.
The fix: tell the renderer to keep them. In CSS, print-color-adjust: exact (with the -webkit- prefix for wider support) on the elements that matter. In Puppeteer, pass printBackground: true. Most APIs enable this or expose it as a flag. It is a two second fix that everyone discovers by accident.
3. You are looking at print CSS without realizing it
When a browser prints, it applies @media print rules and stops applying @media screen ones. If your stylesheet, or a CSS framework you inherited, contains print rules written years ago by someone else, they are now in charge of your PDF. This is why a page can look perfect on screen and lose its entire sidebar in the PDF.
The fix: go and read your print stylesheet. Actually read it. Then either write print rules deliberately, or force the renderer to use screen media (Puppeteer exposes emulateMediaType('screen')) so what you see is what you get. Choose one on purpose rather than discovering which one you got.
4. Fonts fall back to something ugly
Web fonts load asynchronously. If the render fires first, the PDF ships in a fallback face, and because font metrics differ, the fallback also reflows your layout. Suddenly a heading wraps to two lines and pushes everything down a page.
The fix: wait for document.fonts.ready, or self host the fonts so they load from the same origin, or embed them. Do not let a third party font CDN sit inside a rendering pipeline you need to be deterministic. If a document must render identically every time, every asset it depends on should be under your control. Each of those three fixes, including embedding a typeface as a base64 data URI so nothing is fetched at all, is worked through in the guide to fonts not loading in HTML to PDF.
5. The layout is the wrong width entirely
Your page is responsive. The renderer opens it at some default viewport, and your CSS obligingly serves the tablet or mobile breakpoint. Now the PDF is a single narrow column of your mobile site, which is not what anyone wanted.
The fix: set the viewport explicitly before rendering, to a width that triggers the desktop breakpoint you intend. And remember the paper is a separate concept: viewport controls which CSS breakpoint applies, page format controls the sheet the content is poured onto. Get both right or you will fix one and be confused by the other.
6. Tables and blocks split across pages in stupid places
A scrolling page has no concept of a page break. When you paginate it, content lands wherever the math falls: a header at the bottom of one page with its paragraph on the next, a table row orphaned alone, a chart guillotined in half.
The fix: print CSS was built for this and is genuinely good at it.
break-inside: avoidon table rows, cards and figures you never want split.break-after: avoidon headings, so they cannot be stranded at the foot of a page.orphansandwidowsto stop single lines being left behind.theadrepeats across pages automatically if your table markup uses it properly, which is a good reason to use it properly.
Chromium supports a useful subset of this. It does not support all of it. If you need running headers that pull the current chapter title, page counters or cross references that resolve to page numbers, no browser engine will do it and you want a real print engine like PrinceXML instead. We say so plainly in our comparison of the seven HTML to PDF APIs, including where a competitor beats us on exactly this.
7. It renders fine locally and wrong in production
The classic. It works on your MacBook and breaks in the Docker container, usually in one of three ways: emoji and non Latin scripts render as boxes because the slim base image ships no fonts for them, your local Chrome is three versions ahead of the one in the image, or the container has less memory than a heavy page needs and the render dies halfway.
The fix: install the font packages you actually need into the image, pin the browser version, and give the container real memory. Then accept that you now maintain a browser image, with its own patch cadence and its own drift, forever. If you are going to keep that fleet, you may as well be provisioning and deploying those servers with something that does zero downtime releases properly, because you will be doing it more often than you expect.
This category of failure is the honest reason managed rendering exists. Not because rendering is hard, but because keeping a browser identical across environments, forever, is a job nobody signed up for. We ran the full cost breakdown in the Puppeteer alternative comparison, and it does not always come out in our favor.
A debugging order that works
When a PDF looks wrong, resist the urge to change five things. Go in this order, because each step rules out a whole class of cause.
- Screenshot the page with the same renderer, at the same viewport, before printing. If the screenshot is already wrong, it is a timing or CSS problem, not a PDF problem. This single step finds most bugs in a minute.
- If the screenshot is right but the PDF is wrong, it is print media, backgrounds or pagination. Look at your
@media printrules first. - If the screenshot is empty or half drawn, it is timing. Add an explicit wait for the element that proves the content arrived.
- If it only breaks in production, it is the environment: fonts, browser version or memory. Compare the container to your laptop.
Almost every rendering bug we have ever seen is caught by step one or step three.
The failure you cannot debug later
One last thing, and it is the one people only learn the hard way. Every fix above is about making the PDF look right today. None of them tell you what the page actually looked like on the day you rendered it.
When a customer disputes a price, or an auditor asks what your terms said in March, a regenerated PDF is worthless: it reflects today's template and today's data. The only thing that answers the question is a dated capture taken at render time. That is why our renderer stores one alongside the document when you ask it to, so the timestamped snapshot exists whether or not anyone ever needs it. You cannot backfill a record of a moment that has passed, and the day you need it is never the day you thought to start keeping it.
If you want to see how your own markup renders before changing anything, the in-browser converter on this site is free and takes pasted HTML, and the PDF generator API covers the server side version of the same job.
Written by the team building Sitepdf, an HTML to PDF API that archives every page it renders. The in-browser converter is free to try; early access locks the launch pricing.