/// ENGINEERING

Why Your HTML to PDF Layout Breaks (and How to Fix It)

Page breaks, missing backgrounds, substituted fonts, broken tables — the seven problems that make HTML to PDF output differ from the browser, and the CSS that fixes each one.

Document automation engineers, ASHDOCS
Published:
Last updated:

HTML to PDF output differs from the browser for five predictable reasons: background colours are suppressed by default, page breaks are not controlled unless you specify them, table headers do not repeat across pages, container fonts differ from your machine's, and screen units do not map cleanly to physical pages.

Each has a specific fix. This post covers all of them, in the order people usually hit them.

Why are my background colours missing?

The most common problem, and the simplest fix.

Print rendering suppresses backgrounds by default — a convention inherited from browsers, intended to save ink. Your shaded table rows, coloured headers and dark call-out boxes render as white.

Two fixes, and you generally want both.

In your rendering options:

{ "printBackground": true }

And in your CSS, as a fallback for renderers that respect it:

* {
  -webkit-print-color-adjust: exact;
  print-color-adjust: exact;
}

If you fix only one thing from this post, fix this. It accounts for most "my PDF looks wrong" reports.

How do I control page breaks?

By default, content flows and breaks wherever the page happens to end — through the middle of a table row, between a heading and its paragraph, across an invoice line item.

/* Never split these across pages */
tr, .invoice-item, .card, figure {
  page-break-inside: avoid;
  break-inside: avoid;
}

/* Keep a heading with the content that follows it */
h1, h2, h3 {
  page-break-after: avoid;
  break-after: avoid;
}

/* Force a break before a section */
.new-section {
  page-break-before: always;
  break-before: page;
}

/* Avoid orphans and widows */
p {
  orphans: 3;
  widows: 3;
}

Include both the legacy page-break-* and modern break-* properties. Renderers vary in which they honour, and specifying both costs nothing.

Why do my table headers disappear after page one?

Because a <thead> only repeats if you tell it to:

thead { display: table-header-group; }
tfoot { display: table-footer-group; }

One line. A twelve-page invoice goes from unreadable to correct.

This requires real <table> markup. A CSS grid pretending to be a table cannot repeat headers, because the renderer has no concept of a header row. Use semantic tables for tabular data — it fixes this, and answer engines extract them far more readily.

Why are my fonts wrong?

A rendering container has almost no fonts installed. When your CSS asks for a font that is not present, the renderer substitutes — and the substitute has different metrics, so your careful layout shifts.

Load fonts explicitly:

@font-face {
  font-family: 'Inter';
  src: url('https://your-cdn.com/inter-regular.woff2') format('woff2');
  font-weight: 400;
  font-display: block;
}
body { font-family: 'Inter', sans-serif; }

Use font-display: block rather than swap. In a print render you want the correct font or a wait — not a flash of a fallback captured mid-swap.

Currency symbols and non-Latin scripts fail silently. A container with only basic Latin glyphs renders ₹, €, or Devanagari as boxes. If your documents use them, embed a font that includes them.

How do I set margins and page size?

Browser print dialogue settings do not exist server-side. Set them in CSS:

@page {
  size: A4 portrait;
  margin: 20mm 15mm;
}
@page :first {
  margin-top: 40mm;   /* room for a letterhead */
}

Use physical units — mm, cm, in, pt — not pixels. A pixel has no fixed physical size, and the mapping to a printed page varies by renderer.

Why don't my images appear?

The renderer fetches images over the public internet, from a container that is not your machine and not your user's browser.

These will not resolve:

Host template assets on a normal web server or a public bucket, and reference them by absolute URL. For small logos, a base64 data URI removes the network fetch entirely and is the most reliable option.

Quick reference

SymptomFix
Backgrounds and colours missingprintBackground: true + print-color-adjust: exact
Content splits mid-rowpage-break-inside: avoid
Table header only on page 1thead \{ display: table-header-group \}
Wrong or substituted font@font-face with an absolute URL
Symbols render as boxesEmbed a font with those glyphs
Margins ignored@page \{ margin: 20mm \}
Images missingAbsolute public URLs, or base64
Heading alone at page bottompage-break-after: avoid on headings
Layout wrong sizeReplace vh/vw with mm

Frequently asked questions

Why does my HTML to PDF output look different from the browser?

Print rendering suppresses backgrounds by default, applies no page-break rules unless specified, uses only the fonts installed in the rendering container, and ignores browser print-dialogue settings. Each difference has a specific CSS or option fix.

How do I stop a table row splitting across pages?

Apply page-break-inside: avoid and break-inside: avoid to the <tr> elements. Include both properties, as renderers differ in which they honour.

How do I repeat a table header on every page?

Set thead { display: table-header-group; } in your CSS. This requires real <table> markup — a CSS grid cannot repeat headers.

Why are my background colours not printing?

Backgrounds are suppressed by default in print rendering. Enable printBackground in your render options and add print-color-adjust: exact to your CSS.

How do I set custom page margins?

Use the CSS @page rule with physical units: @page { size: A4; margin: 20mm 15mm; }. Browser print settings do not apply to server-side rendering.

In short

Backgrounds need printBackground. Rows need page-break-inside: avoid. Table headers need table-header-group. Fonts must be loaded explicitly. Margins go in @page, in millimetres. Images need absolute public URLs.

Six rules cover almost every difference between what you see in the browser and what lands in the PDF.