·6 min read

CSS for print: styling HTML so it becomes a clean PDF

PDF rendering engines apply CSS's print media rules, not the screen rules your browser normally shows you — so a page that looks fine in Chrome can come out with cut-off tables, missing background colors, or arbitrary page breaks. The fix is to write explicit @page rules for size and margins, use break-inside: avoid on things that shouldn't split (rows, cards, images), and force print-color-adjust: exact if you want backgrounds and colors to survive. Get those three right and most "my PDF looks broken" bugs disappear.

Why your PDF doesn't look like your browser preview

Every HTML-to-PDF API — including Docweave — renders your markup with a real browser engine (Chromium) in "print" emulation, not "screen" emulation. That single distinction explains almost every layout surprise people report: @media screen styles get ignored, 100vh behaves differently because there's no viewport in the traditional sense, and browser default print stylesheets (which strip background colors and shrink margins) kick in unless you override them. If you've only ever tested by eyeballing the page in a browser tab, you've been testing the wrong render path.

The good news: print CSS is a small, stable spec. You need to get comfortable with maybe eight properties. This post covers the ones that actually matter for turning arbitrary HTML into a document that looks intentional.

Set page size and margins with @page

Don't rely on the renderer's default margins — declare them explicitly. The @page at-rule controls page box dimensions and margins independently of your body CSS:

@page {
  size: A4; /* or: letter, legal, 210mm 297mm, etc. */
  margin: 20mm 15mm 25mm 15mm; /* top right bottom left */
}

/* Give the first page a different margin, e.g. for a letterhead */
@page :first {
  margin-top: 40mm;
}

Use physical units (mm, in, pt) for anything print-related, not px or vh. px is a screen concept and while Chromium will convert it at a fixed 96dpi ratio, thinking in millimeters or points keeps you honest about what will actually print. This matters most for invoices and contracts, where a client will notice if your margins are inconsistent between page 1 and page 2.

If you're building templates that get filled with JSON data — line items, signatures, dynamic paragraph counts — pair this with a content-length-agnostic approach: never assume a fixed number of pages. Structure your CSS so it degrades gracefully whether the content is one page or ten.

Controlling where pages break

This is the single most common print-CSS problem: a table row, a card, or a heading gets cleaved in half at a page boundary because the browser had no instruction not to do that. The modern properties are break-inside, break-before, and break-after (the older page-break-* aliases still work in Chromium and are worth including for safety):

/* Never split these elements across a page boundary */
.invoice-line,
.card,
table tr,
figure {
  break-inside: avoid;
  page-break-inside: avoid; /* legacy alias, harmless to include */
}

/* Force a new page before major sections */
.section-cover,
.chapter {
  break-before: page;
}

/* Keep a heading glued to the paragraph that follows it */
h2, h3 {
  break-after: avoid;
}

A few gotchas worth knowing up front:

  • break-inside: avoid on an element taller than one full page is a no-op — the browser has to split it somewhere. Keep individual blocks shorter than your page height, or design for legitimate splits inside long content (e.g. a long <table> body).
  • Flexbox and CSS Grid containers historically ignored break rules in some engines. Chromium has improved this, but if you see a flex row splitting oddly, try converting the outer wrapper to display: block and only using flex/grid for the inner layout.
  • orphans and widows (e.g. orphans: 3; widows: 3;) control how many lines of a paragraph can be stranded alone at the top or bottom of a page — useful for long-form reports with body text.

Stopping content from getting cut off at the edge

"Cut off" content is almost always one of three causes: an element wider than the page's content box, overflow: hidden on a container that's taller than a page, or a fixed-height container that clips its own children. For PDF rendering, treat overflow: hidden as a code smell unless you're certain the content fits — it silently clips instead of pushing to a new page.

/* Bad: silently clips if content grows */
.container {
  height: 900px;
  overflow: hidden;
}

/* Better: let height be intrinsic, let content flow across pages */
.container {
  min-height: 900px;
}

Also check horizontal overflow: wide tables (common in quotes and financial reports) are the usual culprit. Set table-layout: fixed with explicit column widths, or scale down font size at a smaller breakpoint, rather than letting a table silently spill past the margin — Chromium won't warn you, it will just render past the page edge and the overflow will be invisible in the output unless you specifically check the trailing pages.

Web fonts: make sure they actually load before rendering

If your PDFs come out in a fallback serif font instead of your brand typeface, the render almost certainly fired before the web font finished downloading. This is a timing problem, not a CSS problem, but it's worth solving here because it's so common. Two approaches:

  1. Self-host the font file and reference it with @font-face and a real URL your renderer can resolve at request time, rather than depending on a third-party font CDN.
  2. If you control the render call directly (via Node, Python, or another SDK), make sure your HTML source waits for document.fonts.ready before treating the page as "loaded" — Docweave's Chromium engine already waits for network idle and font loading by default, so this mostly matters if you're building a custom renderer.

Test with a font that has a visually distinct fallback (a condensed display font, for example) — if the fallback ever renders, you'll notice immediately instead of shipping subtly-wrong PDFs to customers.

Getting background colors and images to actually print

Browsers default to stripping background colors and images when printing, on the theory that users don't want to burn through printer ink on a colored banner. That default is almost never what you want for a generated PDF — a colored invoice header or a certificate background is part of the design. Override it explicitly:

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

Put this at the top of your stylesheet, applied broadly (the universal selector is fine here — it's a low-cost property). Without it, a beautifully designed certificate template with a gradient background will render as a plain white page with only the text visible, which is a confusing bug to track down if you don't know this default exists.

A minimal print stylesheet to start from

@page {
  size: A4;
  margin: 20mm 15mm;
}

* {
  -webkit-print-color-adjust: exact;
  print-color-adjust: exact;
  box-sizing: border-box;
}

body {
  font-family: 'Inter', sans-serif;
  color: #1a1a1a;
}

table tr, .card, figure {
  break-inside: avoid;
}

h1, h2, h3 {
  break-after: avoid;
}

.page-break {
  break-before: page;
}

That's a reasonable default for invoices, quotes, and reports alike. From there, tune margins and break rules per template as you see real output — it's much faster to iterate against actual rendered PDFs than to guess.

Rendering it with Docweave

Once your HTML and print CSS are solid, send them straight through the API:

curl https://docweave.dev/api/v1/pdf \
  -H "Authorization: Bearer dw_live_your_key" \
  -H "Content-Type: application/json" \
  -H "Accept: application/pdf" \
  -d '{
    "source": { "type": "html", "html": "<html>...</html>" },
    "idempotencyKey": "invoice-2026-0714-001"
  }' \
  --output document.pdf

Or from the MCP server, if you're generating documents from an agent workflow:

await generate_pdf({
  source: { type: "html", html: htmlString },
  outputPath: "./document.pdf"
});

Because the idempotencyKey covers retries, it's safe to re-run the same render while you're iterating on CSS without worrying about being billed twice for the same document. If you want to see the raw structure of the response in a language other than curl or JS, the docs cover request/response shapes for every source type, and the PDF cost calculator is useful if you're estimating volume before committing to a plan.

FAQ

Why does my background color disappear in the generated PDF?

Browsers default to stripping backgrounds when printing to save ink. Add print-color-adjust: exact; (and the -webkit- prefixed version for Chromium) to your CSS to force colors and background images to render.

How do I stop a table row from splitting across two pages?

Apply break-inside: avoid; to the row or card element. It won't help if the element is taller than a full page — in that case the browser has no choice but to split it somewhere.

What units should I use for page size and margins?

Use physical units — mm, in, or pt — inside @page rules rather than px or viewport units. Print layout is about physical page dimensions, and thinking in those units avoids confusion when margins look right on screen but wrong on paper.

My custom web font isn't showing up in the PDF — why?

This is usually a font-loading timing issue: the render fired before the font finished downloading. Self-host the font file so it resolves reliably, and if you're building a custom render pipeline, wait on document.fonts.ready before considering the page ready to render.

Related

Generate your first PDF in minutes.

Get an API key