You've built a responsive web app and now need to export pages as PDFs. The first attempt produces a mobile-layout PDF with collapsed columns, broken images, and missing backgrounds. Sound familiar?
Responsive CSS and PDF rendering engines have fundamentally different mental models. This guide explains the design patterns that bridge that gap — covering @media print, viewport configuration, Flexbox/Grid behavior in PDFs, paper size control, and a complete template you can use as a starting point with FUNBREW PDF or any Chromium-based PDF engine.
Why Responsive Designs Break in PDF
Before writing any CSS, it helps to understand how a PDF engine renders HTML.
The Rendering Model Difference
| Aspect | Web browser | PDF engine |
|---|---|---|
| Canvas | Infinite scrollable height | Fixed page size (A4, Letter…) |
| Width | Variable viewport | Fixed content width |
| Interaction | Hover, click, scroll | None |
| Media type | screen |
print |
| Page breaks | Don't exist | Must be explicitly controlled |
| Fonts | System + web fonts | Engine-dependent |
A Chromium-based PDF engine renders HTML as a headless browser, then slices the rendered output into pages. The viewport width at render time directly affects which responsive breakpoints apply.
The Breakpoint Problem
Consider this typical responsive CSS:
/* Desktop: two columns */
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
/* Tablet and below: single column */
@media (max-width: 768px) {
.grid {
grid-template-columns: 1fr;
}
}
If the PDF engine renders at a default viewport of 600px — which can happen when no explicit viewport is set — the grid collapses to a single column. The fix is simple: tell the engine what viewport width to use.
Using @media print Correctly
The PDF engine renders using the print media type. @media print is where all your PDF-specific styles should live.
The Core Pattern: Separate Screen and PDF Styles
/* ========================================
Shared base styles
======================================== */
:root {
--color-primary: #2563eb;
--color-text: #1e293b;
--font-base: 'Inter', system-ui, sans-serif;
}
body {
font-family: var(--font-base);
color: var(--color-text);
}
/* ========================================
PDF-specific styles (inside @media print)
======================================== */
@media print {
/* Reset for print */
body {
background: #ffffff;
font-size: 10.5pt;
line-height: 1.6;
color: #000000;
}
/* Hide navigation, sidebars, ads */
nav, aside, .sidebar, .cookie-banner,
.chat-widget, .no-print {
display: none !important;
}
/* Make content full-width */
.main-content {
width: 100% !important;
max-width: none !important;
margin: 0 !important;
padding: 0 !important;
}
/* Show URLs after external links */
a[href^="http"]::after {
content: " (" attr(href) ")";
font-size: 8pt;
color: #6b7280;
}
/* Hide URL for internal links */
a[href^="/"]::after,
a[href^="#"]::after {
content: none;
}
}
Screen-only and PDF-only Elements
Manage elements that should appear only on screen or only in the PDF:
/* Visible on screen, hidden in PDF */
.screen-only { display: block; }
/* Hidden on screen, visible in PDF */
.pdf-only { display: none; }
@media print {
.screen-only { display: none !important; }
.pdf-only { display: block; }
}
<!-- Interactive header for the browser -->
<header class="screen-only">
<nav>…</nav>
</header>
<!-- PDF cover page (invisible in browser) -->
<div class="pdf-only pdf-cover">
<h1>Project Proposal</h1>
<p>Acme Corp — April 2026</p>
</div>
Color and Contrast Optimization
Light colors that look fine on screen often look washed out in PDFs:
@media print {
/* Convert shadows to borders (shadows don't print well) */
.card, .panel {
box-shadow: none !important;
border: 1px solid #d1d5db !important;
}
/* Force flat backgrounds instead of gradients */
.gradient-header {
background: #2563eb !important;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
/* Darken light gray text */
.text-muted, .text-gray-400 {
color: #374151 !important;
}
/* Strengthen table borders */
table, th, td {
border-color: #374151 !important;
}
}
PDF Layout Design Patterns
Pattern 1: Responsive → Fixed-Width Conversion
The most common pattern. Responsive on screen, fixed layout in PDF.
/* Screen: responsive auto-fit grid */
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
}
/* PDF: fixed 2-column grid */
@media print {
.dashboard-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12mm;
}
.dashboard-grid .card {
border: 1pt solid #e2e8f0;
padding: 8mm;
break-inside: avoid;
box-shadow: none;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}
Pattern 2: Sidebar Layout Conversion
Convert a 3-column screen layout to a PDF-friendly structure:
/* Screen: sidebar + main + aside */
.layout {
display: grid;
grid-template-columns: 240px 1fr 200px;
grid-template-areas: "sidebar main aside";
gap: 24px;
}
/* PDF: hide sidebar, collapse aside below main */
@media print {
.layout {
display: block;
}
.layout .sidebar {
display: none;
}
.layout .aside {
border-top: 1pt solid #e2e8f0;
padding-top: 8mm;
margin-top: 8mm;
columns: 2; /* Two-column for compact supplemental info */
column-gap: 12mm;
}
}
Pattern 3: Fixed Headers and Footers
Every page of the PDF shows a company logo and page number.
Method 1: CSS @page Margin Boxes (Standard)
@page {
size: A4;
margin: 25mm 20mm 22mm 20mm;
/* Page number at bottom center */
@bottom-center {
content: counter(page) " / " counter(pages);
font-size: 9pt;
color: #6b7280;
font-family: 'Inter', sans-serif;
}
/* Company name at bottom left */
@bottom-left {
content: "Acme Corp";
font-size: 8pt;
color: #9ca3af;
}
/* Date at top right */
@top-right {
content: "April 2026";
font-size: 8pt;
color: #9ca3af;
}
}
/* No header/footer on the cover page */
@page :first {
@bottom-center { content: none; }
@bottom-left { content: none; }
@top-right { content: none; }
}
Method 2: FUNBREW PDF headerHtml / footerHtml Options
For dynamic data in headers/footers (chapter titles, usernames), use the API option:
const response = await fetch('https://pdf.funbrew.cloud/api/v1/generate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.FUNBREW_PDF_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
html: mainContent,
options: {
format: 'A4',
margin: { top: '25mm', right: '20mm', bottom: '22mm', left: '20mm' },
headerHtml: `
<div style="
width: 100%;
font-size: 9pt;
color: #6b7280;
font-family: 'Inter', sans-serif;
display: flex;
justify-content: space-between;
padding: 0 20mm;
">
<span>Project Proposal v2.1</span>
<span>April 2026</span>
</div>
`,
footerHtml: `
<div style="
width: 100%;
font-size: 9pt;
color: #6b7280;
font-family: 'Inter', sans-serif;
text-align: center;
padding: 0 20mm;
">
<span class="pageNumber"></span> / <span class="totalPages"></span>
</div>
`,
},
}),
});
<span class="pageNumber">and<span class="totalPages">are automatically replaced with the current page number and total page count. See the API docs for details.
Viewport Width Configuration
This is the most impactful setting for responsive PDF output. The viewport width determines which CSS breakpoints apply.
Setting Viewport Width in FUNBREW PDF
const options = {
format: 'A4',
viewport: {
width: 1280, // Force desktop layout
height: 900, // Height doesn't matter much (content scrolls)
deviceScaleFactor: 1,
},
};
Choosing the Right Viewport Width
| Viewport width | Best for |
|---|---|
| 800–1024px | A4-optimized documents and reports |
| 1280px | General desktop layouts |
| 1440px | Wide dashboard layouts |
| 375px | Intentionally mobile-style output |
The Math: Viewport Width vs. A4 Content Width
A4 portrait = 210mm × 297mm
At 96 dpi: ≈ 794px × 1123px
With 20mm left/right margins:
Content width = 210mm − 40mm = 170mm ≈ 643px
Optimal viewport: 800–1000px maps cleanly to A4 content width
A utility to compute the optimal viewport:
function calcOptimalViewport(format, marginMm) {
const PAGE_SIZES = {
A4: { width: 210 },
Letter: { width: 215.9 },
A3: { width: 297 },
};
const page = PAGE_SIZES[format];
// 1mm = 3.7795px at 96 dpi
const totalWidthPx = Math.round(page.width * 3.7795);
return totalWidthPx; // Use the full paper width as the viewport
}
console.log(calcOptimalViewport('A4', {})); // → 794
Combining Viewport and @media print
When you render at 800px, here is what happens with breakpoints:
/* Applied (800px ≥ 768px) */
@media (min-width: 768px) {
.grid { grid-template-columns: 1fr 1fr; }
}
/* Not applied (800px < 1024px) */
@media (min-width: 1024px) {
.grid { grid-template-columns: 1fr 1fr 1fr; }
}
/* Recommended: override explicitly inside @media print */
@media print {
.grid {
grid-template-columns: 1fr 1fr; /* Independent of viewport */
}
}
Always use @media print overrides so your PDF layout doesn't silently change when someone adjusts the viewport setting.
Flexbox and Grid in PDF
Chromium-based engines support modern CSS layout well, but there are some behavioral differences worth knowing.
Flexbox in PDF
/* Basic Flexbox — fully supported */
.flex-container {
display: flex;
gap: 16px;
align-items: flex-start;
}
@media print {
/* Disable wrapping: flex-wrap across page breaks can be unpredictable */
.flex-container {
flex-wrap: nowrap;
}
/* flex: 1 equal distribution works correctly */
.flex-item {
flex: 1;
min-width: 0; /* Prevent overflow */
}
/* Convert horizontal to vertical layout for PDF */
.sidebar-layout {
display: flex;
flex-direction: column;
}
}
CSS Grid in PDF
/* CSS Grid — fully supported */
.report-grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 20px;
}
@media print {
/* Use min-content rows to avoid awkward whitespace */
.report-grid {
grid-auto-rows: min-content;
}
/* Prevent individual items from splitting across pages */
.grid-item {
break-inside: avoid;
}
/* Full-width items start on a new page */
.grid-item--full {
grid-column: 1 / -1;
break-before: page;
}
}
CSS Properties That Need Special Handling
/* position: sticky — has no meaning in PDF, reset it */
.sticky-header { position: sticky; top: 0; }
@media print {
.sticky-header { position: static; }
}
/* vh/vw — depends on viewport, avoid for sized elements */
@media print {
.hero {
height: auto;
min-height: 0;
}
}
/* backdrop-filter — variable support, provide fallback */
@media print {
.blur-bg {
backdrop-filter: none;
background: rgba(255, 255, 255, 0.95);
}
}
/* CSS Animations/Transitions — snapshot at render time, not an issue */
/* CSS Custom Properties — fully supported in Chromium */
Image Size Control
Base Rules for All Images
img {
max-width: 100%;
height: auto;
display: block;
}
@media print {
img {
max-width: 100% !important;
page-break-inside: avoid;
}
/* Max image height for A4 with 25mm top/bottom margins */
img.full-page {
max-height: 247mm; /* 297mm − 50mm */
width: auto;
max-width: 170mm;
object-fit: contain;
}
/* Side-by-side image group */
.image-group {
display: flex;
gap: 10mm;
break-inside: avoid;
}
.image-group img {
flex: 1;
max-width: calc(50% - 5mm);
height: auto;
}
}
Preserving Background Colors
/* Force background colors to print */
.branded-header {
background-color: #2563eb;
color: white;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
.chart-container {
background: white;
border: 1px solid #e2e8f0;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
Canvas Charts — Use Image Fallbacks
Canvas-based charts (Chart.js, D3, etc.) render as rasterized images when printed. For best results, export the chart to PNG/SVG and show that in PDF:
@media print {
.chart-canvas { display: none; }
.chart-image { display: block; max-width: 100%; }
}
<canvas class="chart-canvas" id="salesChart"></canvas>
<img class="chart-image" src="" id="salesChartImg" alt="Sales chart">
<script>
const chart = new Chart(document.getElementById('salesChart'), { /* … */ });
// Export to PNG for PDF output
document.getElementById('salesChartImg').src = chart.toBase64Image();
</script>
Paper Size Control
@page Rule
/* A4 portrait (default) */
@page {
size: A4 portrait;
margin: 20mm 15mm 18mm 15mm;
}
/* A4 landscape */
@page {
size: A4 landscape;
margin: 15mm 20mm;
}
/* US Letter */
@page {
size: Letter;
margin: 1in 1in;
}
/* Custom size (e.g. business card) */
@page {
size: 90mm 55mm;
margin: 5mm;
}
Mixing Portrait and Landscape Pages
Use named pages to combine orientations in one document:
@page { size: A4 portrait; margin: 20mm 15mm; }
@page landscape-page {
size: A4 landscape;
margin: 15mm 20mm;
}
.landscape-section {
page: landscape-page;
break-before: page;
break-after: page;
}
<section>
<h2>Chapter 1 — Overview</h2>
<p>Portrait content…</p>
</section>
<section class="landscape-section">
<h2>Chapter 2 — Revenue Chart</h2>
<!-- Wide tables and charts go here -->
<table class="wide-table">…</table>
</section>
<section>
<h2>Chapter 3 — Conclusion</h2>
<p>Back to portrait…</p>
</section>
Setting Paper Size via API
const options = {
format: 'A4', // A4, Letter, A3, A5…
landscape: false, // true for landscape
margin: {
top: '20mm',
right: '15mm',
bottom: '18mm',
left: '15mm',
},
};
When CSS
@page { size: … }and the APIformatoption conflict, the API setting takes precedence. Pick one approach and be consistent.
Complete Template: Web and PDF from One HTML File
Here is a production-ready monthly report template that renders well on screen and produces a clean A4 PDF.
HTML + CSS Template
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Monthly Report — April 2026</title>
<style>
/* ========================================
CSS Variables
======================================== */
:root {
--color-primary: #2563eb;
--color-secondary: #64748b;
--color-border: #e2e8f0;
--color-bg-light: #f8fafc;
--font-base: 'Inter', system-ui, sans-serif;
}
/* ========================================
Shared Base
======================================== */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: var(--font-base);
color: #1e293b;
line-height: 1.6;
}
h1 { font-size: 24px; font-weight: 700; color: var(--color-primary); }
h2 { font-size: 18px; font-weight: 700; margin: 24px 0 12px; }
p { margin-bottom: 12px; }
table { width: 100%; border-collapse: collapse; font-size: 14px; }
th {
background: var(--color-bg-light);
font-weight: 700;
text-align: left;
padding: 10px 12px;
border-bottom: 2px solid var(--color-border);
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
td { padding: 8px 12px; border-bottom: 1px solid var(--color-border); }
/* ========================================
Screen-only Styles
======================================== */
@media screen {
body { background: #f1f5f9; }
.page-wrapper {
max-width: 1200px;
margin: 0 auto;
padding: 24px;
}
.screen-nav {
background: white;
border-bottom: 1px solid var(--color-border);
padding: 16px 24px;
margin-bottom: 24px;
display: flex;
justify-content: space-between;
align-items: center;
}
.kpi-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.kpi-card {
background: white;
border-radius: 8px;
padding: 20px;
border: 1px solid var(--color-border);
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
}
/* ========================================
PDF-only Styles (@media print)
======================================== */
@media print {
/* Paper setup */
@page {
size: A4;
margin: 20mm 15mm 18mm 15mm;
@bottom-center {
content: counter(page) " / " counter(pages);
font-size: 9pt;
color: #6b7280;
font-family: 'Inter', sans-serif;
}
@bottom-left {
content: "Monthly Report — April 2026";
font-size: 8pt;
color: #9ca3af;
}
}
@page :first {
@bottom-center { content: none; }
@bottom-left { content: none; }
}
/* Base reset */
body { background: white; font-size: 10pt; line-height: 1.5; }
/* Remove screen chrome */
.screen-nav, .no-print, .screen-only { display: none !important; }
/* Remove screen wrapper constraints */
.page-wrapper { max-width: none; margin: 0; padding: 0; }
/* 2-column KPI grid */
.kpi-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8mm;
margin-bottom: 8mm;
}
.kpi-card {
border: 1pt solid #e2e8f0;
padding: 6mm;
break-inside: avoid;
box-shadow: none;
border-radius: 0;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
/* Table page-break control */
table { break-inside: auto; font-size: 9pt; }
thead { display: table-header-group; }
tfoot { display: table-footer-group; }
tr { break-inside: avoid; }
/* Headings */
h1 { font-size: 18pt; }
h2 { font-size: 13pt; break-after: avoid; }
/* New page for each major section */
.report-section + .report-section { break-before: page; }
}
/* ========================================
Shared Components
======================================== */
.kpi-label { font-size: 12px; color: var(--color-secondary); margin-bottom: 4px; }
.kpi-value { font-size: 28px; font-weight: 700; color: var(--color-primary); }
.kpi-change { font-size: 12px; margin-top: 4px; }
.kpi-change.up { color: #16a34a; }
.kpi-change.down { color: #dc2626; }
</style>
</head>
<body>
<nav class="screen-nav screen-only">
<span>Monthly Report System</span>
<button onclick="window.print()" class="no-print">Save as PDF</button>
</nav>
<div class="page-wrapper">
<header>
<h1>Monthly Performance Report</h1>
<p style="color:#64748b;">April 2026 | Acme Corp</p>
</header>
<section class="report-section">
<h2>Key Metrics</h2>
<div class="kpi-grid">
<div class="kpi-card">
<div class="kpi-label">Monthly Revenue</div>
<div class="kpi-value">$428K</div>
<div class="kpi-change up">▲ 12.3% vs last month</div>
</div>
<div class="kpi-card">
<div class="kpi-label">New Customers</div>
<div class="kpi-value">124</div>
<div class="kpi-change up">▲ 8.7% vs last month</div>
</div>
<div class="kpi-card">
<div class="kpi-label">NPS Score</div>
<div class="kpi-value">72</div>
<div class="kpi-change up">▲ 4pts</div>
</div>
<div class="kpi-card">
<div class="kpi-label">Churn Rate</div>
<div class="kpi-value">1.2%</div>
<div class="kpi-change up">▼ 0.3pts (improved)</div>
</div>
</div>
</section>
<section class="report-section">
<h2>Revenue Breakdown</h2>
<table>
<thead>
<tr>
<th>Product</th>
<th>Revenue</th>
<th>vs Last Month</th>
<th>vs Target</th>
</tr>
</thead>
<tbody>
<tr><td>Enterprise</td><td>$240K</td><td>+15.2%</td><td>106%</td></tr>
<tr><td>Standard</td> <td>$128K</td><td>+8.1%</td> <td>98%</td></tr>
<tr><td>Starter</td> <td>$60K</td> <td>+22.4%</td><td>120%</td></tr>
</tbody>
<tfoot>
<tr style="font-weight:700;">
<td>Total</td><td>$428K</td><td>+12.3%</td><td>104%</td>
</tr>
</tfoot>
</table>
</section>
</div>
</body>
</html>
Generating the PDF with Node.js
const fs = require('fs');
const fetch = require('node-fetch');
async function generateReport(templatePath, outputPath, data) {
let html = fs.readFileSync(templatePath, 'utf-8');
// Inject dynamic data
html = html.replace('$428K', `$${data.revenue}`);
html = html.replace('April 2026', data.month);
const response = await fetch('https://pdf.funbrew.cloud/api/v1/generate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.FUNBREW_PDF_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
html,
options: {
format: 'A4',
viewport: { width: 1280, height: 900 },
margin: { top: '20mm', right: '15mm', bottom: '18mm', left: '15mm' },
printBackground: true,
},
}),
});
if (!response.ok) {
throw new Error(`PDF generation failed: HTTP ${response.status}`);
}
const buffer = Buffer.from(await response.arrayBuffer());
fs.writeFileSync(outputPath, buffer);
console.log('PDF saved:', outputPath);
return buffer;
}
generateReport(
'./template.html',
'./report-april-2026.pdf',
{ revenue: '428K', month: 'April 2026' }
);
Troubleshooting Reference
| Symptom | Likely cause | Fix |
|---|---|---|
| Mobile layout in PDF | Viewport too narrow | Set viewport.width: 1280 in API options |
| Columns collapse to one | Breakpoint applied | Override layout in @media print |
| Images overflow container | No max-width |
Add img { max-width: 100%; height: auto; } |
| Background colors missing | print-color-adjust not set |
Add print-color-adjust: exact to affected elements |
| Grid breaks unexpectedly | auto-fit recalculates |
Fix column counts explicitly in @media print |
| Font renders as boxes | Font not loaded | Specify a web-safe fallback or use Noto Sans |
| Content cut off | Page break mid-element | Add break-inside: avoid to the element |
| Header/footer missing | @page margins not supported |
Use headerHtml/footerHtml API options instead |
For deeper troubleshooting, see HTML to PDF Troubleshooting. For engine-specific CSS support differences, see wkhtmltopdf vs Chromium.
Summary
Key takeaways for responsive HTML to PDF conversion:
- Viewport width: Set an explicit desktop width (1280px) via the API to force desktop breakpoints
- @media print: Centralize all PDF-specific styles here; never mix screen and print styles
- Explicit layouts: Fix Flexbox/Grid column counts in
@media printso viewport changes don't affect PDF layout - Paper size: Use
@page { size: A4; }for CSS control, or setformatin the API options - Headers/footers: Use
@pagemargin boxes for static content, orheaderHtml/footerHtmlfor dynamic content - Images:
max-width: 100%prevents overflow;print-color-adjust: exactpreserves backgrounds - Page breaks:
break-inside: avoidandbreak-before: pagegive you precise control
Try pasting your HTML into the Playground to preview PDF output in real time. For API usage in different languages, see the Quickstart Guide. For automated report generation, see Business Report PDF Automation.
Related Resources
- CSS Tips for HTML to PDF — Page breaks, fonts, and table handling
- wkhtmltopdf vs Chromium — Engine-by-engine CSS support comparison
- HTML to PDF Troubleshooting — Common errors and solutions
- HTML to PDF Complete Guide — Full overview of conversion approaches
- PDF API Production Guide — Stable production deployment checklist
- Business Report PDF Generation — Automated report implementation patterns
- PDF Template Engine Guide — Combining template engines with PDF APIs
- Playground — Test HTML and CSS with live PDF preview
- API Documentation — FUNBREW PDF API reference