The document is wrong
The PDF ignores my CSS
The document arrives with the right words in the right order and none of the styling. It looks like an unstyled HTML page from 1997, which is exactly what it is.
What is actually happening
The stylesheet is linked rather than embedded, and the link points somewhere the renderer cannot reach: a bundler-generated path, a digested asset filename, or a URL behind your application's authentication. The markup arrives, the stylesheet request 404s, and the browser does what browsers do with a missing stylesheet, which is carry on silently.
Confirming it is this and not something that looks like it
Save the exact HTML string you are sending to a file and open it from your desktop, not from your dev server. If it is unstyled there, the renderer is seeing the same thing you are.
The fix
Inline the CSS into a style tag in the string you send. For a document template this is not the compromise it sounds like: a document has one stylesheet, it is small, and it is never cached across requests anyway. Build it into the template rather than linking it.
import { readFileSync } from "node:fs";
// Read once at module load, not per request.
const documentCss = readFileSync("./templates/document.css", "utf8");
export function wrap(bodyMarkup) {
return `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>${documentCss}</style>
</head>
<body>${bodyMarkup}</body>
</html>`;
}If that was not it
These produce the same symptom often enough to be worth ruling out before assuming the fix above did not work.
- A stylesheet wrapped in @media screen, which correctly does not apply to a paged render
- CSS custom properties defined on a selector that does not match the document root of the string you sent
- Tailwind or another build step whose output file was never generated for this template's class names
- A Content Security Policy on the source page, which is irrelevant to the render but often the first suspect
Frequently asked
Does this happen with every rendering engine?
The behaviour behind it is not specific to one tool. The stylesheet is linked rather than embedded, and the link points somewhere the renderer cannot reach: a bundler-generated path, a digested asset filename, or a URL behind your application's authentication. Anything rendering HTML to a paged medium has to make the same decision, so the fix travels with you if you change how the render happens.
Will this show up as an error in my logs?
No, and that is what makes it expensive. A document that renders wrong is still a successful render as far as every log line is concerned. It is found by someone opening the file, which is usually the customer.
Related failures
Problems people arrive at from the same starting point, or mistake for this one.
Background colours and images are missing from the PDF
Browsers drop backgrounds when printing, because a page with every background painted would empty a toner cartridge, and most render APIs inherit that default.
The generated PDF is blank
Almost always one of three things: the content is rendered by JavaScript that had not run when the snapshot was taken, the body has no height because everything inside it is absolutely positioned or floated, or the markup you sent was empty because the template call returned before the data arrived..
Images do not appear in the PDF
The image src is a path rather than a URL.
Content rendered by JavaScript is missing from the PDF
The snapshot was taken before the script finished.
The PDF renders the mobile layout instead of the desktop one
The render viewport is narrower than your first breakpoint, so your own media queries are doing exactly what you told them to.
CSS for paged documents, and what actually works
Which parts of the paged media specification a browser-based render implements.
Everything that goes wrong, by category
The full list, grouped by where in the pipeline it breaks.
Paste your markup and see the rendered document, without signing up.