The first time I generated invoice PDFs from HTML, the staging version looked perfect and the production version quietly moved the totals block to page two, alone, like it had been exiled. Same HTML, same CSS, different converter. That day I learned the fact that governs all HTML-to-PDF work: there is no single "HTML to PDF." There are several rendering engines with different subsets of CSS, different default margins, and different ideas about when a page should break. The output differences are not bugs in your markup. They are the engines, being themselves.
The three families you will actually meet
- Browser engines (headless Chromium via Puppeteer/Playwright, or hosted tools built on it) render your page the way Chrome does — modern CSS, flexbox and grid, web fonts — then print it. If your invoice looks right in Chrome's print preview, this family will treat you best.
- Dedicated layout engines (WeasyPrint, Prince) implement the CSS paged-media spec more faithfully — running headers, page counters, cross-references — but lag on newer layout CSS.
- Legacy converters (older wkhtmltopdf builds and friends) use ancient WebKit. Flexbox is partial, grid is a rumor, and every project eventually has the "why is this column the whole page" incident.
My current one-off jobs go through an HTML-to-PDF converter built on a browser engine; in pipelines I pin one engine in the dependency file and treat changing it as a migration, with visual diffs, not an upgrade.
The CSS that actually controls a printed page
Most invoice-print bugs are five properties wearing trench coats:
-
@page { size: A4; margin: ... }— set the paper and margins in CSS, or accept each engine's defaults, which differ. US Letter vs A4 alone explains a class of "it fits here but not there." -
break-inside: avoidon blocks that must not split — totals, signature areas, table rows groups. Without it, every engine breaks content where the page happens to end. -
theadrepetition — browsers repeat a table's header group on each printed page if you use realtheadmarkup; divs pretending to be headers do not repeat, which is how invoices end up with a page of orphan numbers. -
Print color — backgrounds vanish by default in print CSS (
print-color-adjust: exactrestores the ones you actually want, like a shaded header row). - Fonts the engine can see. A webfont your page loads from a CDN may or may not be fetched by the converter; self-hosting the font or embedding it removes the coin flip, and invoices are not the place for coin flips.
The discipline that ends the surprises
Keep one golden sample — the nastiest invoice you produce: multi-page, long item descriptions, a discount, a tax line, a note in small print — and render it with your pinned engine on every change. Compare page count and eyeball page two; totals drift is visible in seconds. And generate the PDF at the moment of issue with the same engine that renders your preview, so the customer never sees a layout you have not.
HTML-to-PDF is a good architecture — one template, styled with skills your team already has. It just punishes the assumption that print is a screenshot of the screen. Treat the page as a separate medium with its own five rules, pin your engine, and the exiled totals block stays home.
Top comments (0)