Invalid Date

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 API format option 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 print so viewport changes don't affect PDF layout
  • Paper size: Use @page { size: A4; } for CSS control, or set format in the API options
  • Headers/footers: Use @page margin boxes for static content, or headerHtml/footerHtml for dynamic content
  • Images: max-width: 100% prevents overflow; print-color-adjust: exact preserves backgrounds
  • Page breaks: break-inside: avoid and break-before: page give 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

Powered by FUNBREW PDF