How Does CSS Print Logic Work in HTML to PDF Conversion?
8 min read
When you convert a web page to PDF you get a result different from what you saw on screen — and that's not a bug, it's the natural consequence of moving between two different layout models. This article explains how web pages get divided into pages, what CSS's print rules do, and why dynamic content causes trouble.
Two layout models
A web page is a continuous medium. There's no concept of a page; content flows top to bottom as an endless column. The width changes with the browser window and the layout adapts.
PDF is a paged medium. There are fixed-size pages and the content gets distributed across them.
CSS separates the two with the concept of "media type" and lets different rules apply to each.
@media print: two different style sets
Web pages can define different styles for screen and print:
/* For everyone */
body { font-family: sans-serif; }
/* Screen only */
@media screen {
nav { position: fixed; background: #222; }
}
/* Print only */
@media print {
nav, .sidebar, .ads, .comments { display: none; }
body { font-size: 11pt; color: #000; background: #fff; }
a { text-decoration: underline; }
}
PDF generation uses the print rules. On well-designed sites this noticeably cleans up the output:
- Navigation menus, sidebars and ads are hidden.
- Colors become simple and readable.
- Font sizes are set in points.
But it also creates two kinds of problem:
Over-hiding. Some sites hide important information boxes in their print styles too.
No styles at all. Sites that never defined print styles use the screen layout as-is, and the result is usually bad — fixed-position menus can repeat on every page, dark backgrounds can cause trouble.
The physical meaning of CSS units
In a print context CSS units have exact physical values:
| Unit | Physical equivalent | |---|---| | 1in (inch) | 2.54 cm | | 1cm | 1 cm | | 1mm | 1 mm | | 1pt (point) | 1/72 inch | | 1pc (pica) | 12 points | | 1px (CSS pixel) | 1/96 inch |
That last row is critical: 96 CSS pixels is exactly 1 inch.
The consequence of that constant: an A4 page (210 mm = 8.27 inches wide) comes out about 794 CSS pixels wide before margins are subtracted.
And that has a big effect on responsive designs. Many sites use breakpoints like these:
@media (max-width: 768px) { /* tablet and mobile layout */ }
@media (max-width: 1024px) { /* small screen layout */ }
794 pixels is below the second rule and, on some sites, close to the first. The result: in PDF the page drops into a different layout than the one you see on desktop — columns stack, the menu turns into a hamburger icon, images go full width.
That's the answer to "why does the page look like mobile in the PDF?"
Ways around it: use landscape orientation (A4 landscape is around 1123 pixels), reduce the margins, or apply scaling.
@page: defining the page itself
In CSS the @page rule defines the page box:
@page {
size: A4;
margin: 2cm 1.5cm;
}
For size you can use preset sizes (A4, A3, Letter, Legal) or custom dimensions (size: 210mm 297mm). The landscape keyword changes the orientation.
Pseudo-classes let you apply different rules to different pages:
@page :first {
margin-top: 5cm; /* extra space at the top of the cover page */
}
@page :left {
margin-left: 3cm; /* binding allowance */
}
@page :right {
margin-right: 3cm;
}
This is used to leave a binding allowance in book-style double-sided output.
The page-breaking algorithm
The content flow is sliced into chunks the height of a page. When a break point falls in the middle of an element, the browser has to make a decision.
CSS offers properties that steer that decision:
/* Don't split this element */
table, figure, .card { page-break-inside: avoid; }
/* Start a new page before this element */
h1, .chapter { page-break-before: always; }
/* Don't break immediately after this element */
h2, h3 { page-break-after: avoid; }
The third rule is especially useful: it stops a heading from being stranded at the bottom of a page with its text on the next one.
Orphan and widow control:
p {
orphans: 3; /* leave at least 3 lines of a paragraph at the bottom of a page */
widows: 3; /* have at least 3 lines at the top of a page */
}
These are classic typographic rules: a single line of a paragraph stranded at the end or beginning of a page looks bad.
Breaking behavior by element type:
| Element | Breaking behavior | |---|---| | Text block | Breaks at line boundaries | | Table | Breaks at row boundaries | | Image | Can't be split; moves or gets cut | | Positioned element | Unpredictable results | | Flexbox / Grid | Support varies by browser |
The last two rows matter: absolute positioning and modern layout systems don't always behave well in a print context. A complex grid layout can break unexpectedly at a page boundary.
Why background graphics don't print
Print engines by default don't print background colors and images. This is built-in behavior aimed at saving ink.
There's a way to force it in CSS:
* {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
The browser's print dialog also has a "Background graphics" option.
With that setting off, dark-background designs turn into a disaster: white text on a black background becomes white on white once the background isn't printed, and nothing is visible.
If you're preparing your own HTML, inverting the colors in the print styles is a safer approach:
@media print {
.dark-section { background: #fff; color: #000; }
}
Links and interaction
Hyperlinks are usually converted into PDF link annotations and stay clickable. That depends on the converter's capability.
In a printed document you can't see where a link goes. CSS can add the address to the text:
@media print {
a[href^="http"]::after {
content: " (" attr(href) ")";
font-size: 0.8em;
color: #555;
}
}
Form fields usually turn into their static appearance. Some converters can turn HTML forms into PDF form fields, but that isn't a common feature.
JavaScript interactions are lost entirely: dropdowns, tabs, accordions. Content in a closed state doesn't appear in the PDF at all — if an FAQ section is built as an accordion, only the questions show up in the PDF, not the answers.
For that case you need to open the hidden content in the print styles:
@media print {
.accordion-content { display: block !important; }
}
Dynamic content and the timing problem
Most modern web pages fetch content after the page loads: API calls, lazy-loaded images, infinite scroll.
The converter decides at some point that the page is "ready" and starts generating the PDF. Different strategies are used to determine that point:
- The load event: when the page's initial resources have arrived. Usually insufficient.
- Network idle: when no new request has come in for a certain time. A better signal.
- Fixed wait: for example, wait 3 seconds. Crude but simple.
- Element wait: wait until a specific CSS selector appears. The most precise method, but it requires configuration.
On pages using infinite scroll no strategy can deliver the full content — content only loads as the user scrolls, and the converter doesn't scroll.
Lazy-loaded images have the same problem: images that never enter the viewport never load and come out blank in the PDF.
Font loading
Web fonts are downloaded from external sources:
@font-face {
font-family: 'Custom';
src: url('https://fonts.example.com/custom.woff2');
}
If the conversion environment can't reach that resource, the font doesn't load and the browser falls back to a substitute. Because letter widths change, the layout shifts.
There's also a timing problem: if the PDF is generated before the font loads, the page is drawn with the fallback font.
Solutions for critical documents:
- Embed the font into the CSS as base64.
- Use system fonts.
- Add a check that waits for the font to load.
In summary
HTML to PDF conversion is a move from a continuous medium to a paged one, and CSS manages that move with @media print, @page and the page-break properties. Because a CSS pixel is defined as 1/96 of an inch, an A4 page ends up about 794 pixels wide, which causes responsive designs to drop into their mobile layout. Background graphics aren't printed by default; on dark-background designs that's a serious problem. Page-break decisions can be steered with properties like page-break-inside, orphans and widows, but those rules have to be defined in the page's own code. And dynamic content may arrive incomplete depending on when the converter decides the page is "ready" — on pages using infinite scroll and lazy loading, getting the full content is usually impossible.
Frequently Asked Questions
How does a pixel in CSS map to a physical measurement?
In a print context, a CSS pixel is defined as 1/96 of an inch. So 96 pixels is exactly 1 inch, about 2.54 centimeters. Thanks to that constant, an A4 page (210 mm wide) comes out around 794 CSS pixels wide. That's why responsive designs drop into a mobile layout in PDF — 794 pixels sits below the tablet or mobile breakpoint on many sites.
What does the @page rule do?
It defines the page itself: size, orientation and margins. For example @page { size: A4 landscape; margin: 1.5cm; } makes the output A4 landscape with a 1.5 cm margin on every side. You can also apply different rules to the first page or to odd/even pages with the :first, :left and :right pseudo-classes.
How does the page-breaking algorithm decide?
The content flow is sliced into chunks the height of a page. If a break point falls in the middle of an element, the browser first checks the page-break properties (like page-break-inside); if there's no constraint, it splits the element. Text blocks break at line boundaries, tables at row boundaries, and images — which can't be split — either move entirely to the next page or get cut.
Why does JavaScript-generated content sometimes not make it into the PDF?
Because the converter decides at some point that the page is 'ready' and starts generating the PDF. If the content arrives through separate network requests after the page loads and the converter doesn't wait long enough, the output is taken before those requests finish. Some tools wait for network traffic to stop, but on pages using infinite scroll or periodic updates that state never arrives.
Try this out right away with HTML/URL → PDF.
Try HTML/URL → PDF