Prompt
How do I preserve exact layout when converting HTML to PDF?
Latest observation
To preserve exact layout when converting HTML to PDF, the key is to make the PDF engine render the page as close as possible to a real browser page, with fixed dimensions, controlled fonts, and print-specific CSS.
Best practices
1) Use a browser-based renderer
For highest fidelity, use a full browser engine:
- Chrome/Chromium via Puppeteer or Playwright
- WeasyPrint if your layout is mostly static and CSS-driven
- Avoid lightweight HTML-to-PDF tools if pixel-perfect fidelity matters
2) Set an explicit page size
Define the PDF page size instead of relying on defaults:
- Use
@page { size: A4; margin: 0; } - Or specify exact dimensions like
size: 210mm 297mm
If your content is designed for a specific screen size, match that viewport too.
3) Use print CSS
Create a dedicated print stylesheet:
@media print {
body {
margin: 0;
}
}
Also use:
@page {
margin: 0;
}
This prevents browser default margins from shifting your layout.
4) Fix the viewport and scaling
When using Chromium-based tools:
- Set a consistent viewport size
- Disable responsive behavior if you want fixed layout
- Control scale explicitly when generating the PDF
Example with Puppeteer:
await page.setViewport({ width: 1280, height: 720, deviceScaleFactor: 1 });
await page.pdf({
format: 'A4',
printBackground: true,
margin: { top: 0, right: 0, bottom: 0, left: 0 }
});
5) Embed and preload fonts
Layout often shifts because fonts differ between environments.
- Use web fonts or embed fonts directly
- Wait for fonts to load before rendering:
await page.evaluate(() => document.fonts.ready);
- Avoid relying on system fonts unless they’re guaranteed installed
6) Include background colors/images
Many PDF tools omit backgrounds by default.
- Enable background printing:
- Puppeteer:
printBackground: true
- Puppeteer:
- Check transparency and layering carefully
7) Avoid layout-dependent dynamic content
Exact layout is harder if you use:
- Lazy-loaded images
- Animations
- JavaScript that changes DOM after load
- Auto-resizing elements based on async data
Instead:
- Fully render the page first
- Wait for network idle and fonts
- Freeze the DOM before exporting
8) Use fixed sizes instead of fluid ones
Prefer:
px,pt,mmfor print- Explicit widths/heights
box-sizing: border-box
Avoid depending too much on:
%vw/vh- Auto margins in complex nested layouts
- Uncontrolled flex/grid wrapping
9) Watch out for browser print quirks
Some CSS behaves differently in print:
position: fixedmay repeat on every pageoverflow: hiddencan clip contentpage-break-*/break-*rules affect pagination
Useful rules:
.page-break {
break-after: page;
}
10) Test on the same rendering engine
The HTML may look different in:
- Chrome on your machine
- Headless Chrome in CI
- wkhtmltopdf
- Firefox
- Safari
For exact layout, always generate the PDF with the same engine and version in production.
A good default setup with Puppeteer
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 720, deviceScaleFactor: 1 });
await page.goto('https://example.com', { waitUntil: 'networkidle0' });
await page.evaluate(() => document.fonts.ready);
await page.pdf({
path: 'output.pdf',
printBackground: true,
format: 'A4',
margin: { top: 0, right: 0, bottom: 0, left: 0 }
});
await browser.close();
})();
If you need truly identical output
If “exact layout” means no pixel differences at all, consider:
- Designing specifically for PDF pages
- Rendering the content server-side into a fixed-size page
- Using a PDF-native layout library instead of HTML/CSS
- Converting each page to an image and embedding it in PDF if visual fidelity is more important than selectable text
If you want, I can give you:
- a Puppeteer recipe for pixel-perfect PDFs, or
- a checklist for debugging layout shifts in HTML-to-PDF conversion.