ScreenshotNeo

BlogHTML to image & PDF

How to Fix Unexpected Header and Footer Behavior in Puppeteer PDFs

Fix missing, clipped, or overlapping Puppeteer PDF headers and footers with correct options, margins, print CSS, fonts, and debugging steps.

By the ScreenshotNeo team1 October 20268 min read

Start with these three checks: set displayHeaderFooter: true, provide an HTML headerTemplate or footerTemplate, and reserve enough top and bottom margin for those templates. Puppeteer leaves displayHeaderFooter disabled by default, and an undefined margin means no margins are set in the current PDFOptions reference. See the PDFOptions API.

Then check the page format, print-only CSS, CSS @page rules, and font readiness. Page.pdf() renders with print media by default, so the layout can differ from what you see in a normal browser tab. See Puppeteer’s PDF guide.

This complete Node.js example creates a PDF with a visible header, footer, page numbers, explicit margins, and a fixed paper format.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({
  headless: true,
});

try {
  const page = await browser.newPage();
  await page.goto('https://example.com', { waitUntil: 'networkidle0' });

  await page.pdf({
    path: 'output.pdf',
    format: 'A4',
    printBackground: true,
    displayHeaderFooter: true,
    headerTemplate: `
      <div style="width: 100%; font-size: 9px; padding: 0 12mm; color: #555;">
        <span>Example report</span>
      </div>
    `,
    footerTemplate: `
      <div style="width: 100%; font-size: 9px; padding: 0 12mm; color: #555;">
        <span>Page <span class="pageNumber"></span> of <span class="totalPages"></span></span>
      </div>
    `,
    margin: {
      top: '24mm',
      right: '15mm',
      bottom: '22mm',
      left: '15mm',
    },
  });
} finally {
  await browser.close();
}

Run it with:

npm install puppeteer
node generate-pdf.mjs

What each setting controls

Option Purpose Common failure when omitted or wrong
displayHeaderFooter Enables rendering of the templates. Templates are silently absent because the documented default is false.
headerTemplate HTML printed above the page content. Header does not appear, or its HTML is invalid or too tall.
footerTemplate HTML printed below the page content. Footer is missing, clipped, or overlaps the last lines.
margin.top Space reserved for the header. Header overlaps content or is cut off at the page edge.
margin.bottom Space reserved for the footer. Footer overlaps content or disappears outside the printable area.
format Selects a paper preset such as A4 or Letter. Wrapping and available header space differ between environments.
width, height Defines custom paper dimensions. Unexpected scaling or pagination.
preferCSSPageSize Lets CSS @page dimensions take precedence over format, width, and height. Your CSS page size is ignored, or content is scaled to the selected paper.
printBackground Includes background colors and images. Header or document branding appears white.
waitForFonts Controls whether PDF generation waits for fonts; the documented default is true. Text metrics change between runs or custom fonts are not ready.

2. Build templates with Puppeteer’s supported values

Header and footer templates are HTML fragments. Puppeteer documents these special classes:

  • date — generated date
  • title — page title
  • url — page URL
  • pageNumber — current page
  • totalPages — total page count
const headerTemplate = `
  <div style="font-size: 8px; width: 100%; padding: 0 10mm;">
    <span class="title"></span>
    <span style="float: right" class="date"></span>
  </div>
`;

const footerTemplate = `
  <div style="font-size: 8px; width: 100%; padding: 0 10mm;">
    <span class="url"></span>
    <span style="float: right">
      <span class="pageNumber"></span> / <span class="totalPages"></span>
    </span>
  </div>
`;

Keep template CSS self-contained. Treat the template as a small print fragment: set its width, font size, color, padding, and alignment directly rather than relying on the page’s application stylesheet.

3. Reserve enough space and prevent overlap

The header and footer are laid out in the page’s margin areas. A margin that is shorter than the template does not automatically expand to fit it. Increase the relevant margin until the complete template fits.

await page.pdf({
  path: 'report.pdf',
  format: 'Letter',
  displayHeaderFooter: true,
  headerTemplate: '<div style="font-size:10px; padding:0 12mm">Report</div>',
  footerTemplate: '<div style="font-size:10px; padding:0 12mm">Page <span class="pageNumber"></span></div>',
  margin: {
    top: '28mm',
    bottom: '24mm',
    left: '16mm',
    right: '16mm',
  },
});

If content still touches the footer, also inspect the document’s own print CSS. Large headings, fixed-position elements, and explicit page breaks can consume the available space even when the PDF margins are correct.

4. Check paper format and CSS @page rules

Puppeteer’s documented default format is Letter. Select a format explicitly when output must be consistent across machines.

@page {
  size: A4;
  margin: 18mm 14mm 20mm;
}

@media print {
  .screen-only {
    display: none !important;
  }
}

Use preferCSSPageSize: true when the CSS @page size should control the PDF:

await page.pdf({
  path: 'a4-report.pdf',
  displayHeaderFooter: true,
  preferCSSPageSize: true,
  headerTemplate,
  footerTemplate,
  margin: { top: '25mm', bottom: '22mm' },
});

Without that option, Puppeteer fits content to the selected paper size. Mixing a format value with an unexpected CSS page size is a common reason for different wrapping and footer positions.

5. Remember that PDF generation uses print media

The Puppeteer documentation states that Page.pdf() generates the PDF with the print CSS media type. Print styles can hide elements, change widths, alter colors, or replace fonts.

await page.emulateMediaType('screen');
await page.pdf({
  path: 'screen-layout.pdf',
  displayHeaderFooter: true,
  headerTemplate,
  footerTemplate,
  margin: { top: '25mm', bottom: '22mm' },
});

Use screen media only when the PDF should match the screen layout. Otherwise, keep print media and fix the relevant @media print rules.

6. Handle fonts and timing

Puppeteer documents waitForFonts: true as the default. Font loading affects text width, line wrapping, and therefore the number of pages. The documentation also notes that a background page may need Page.bringToFront() for document.fonts.ready to resolve.

await page.bringToFront();
await page.goto('https://example.com/report', { waitUntil: 'networkidle0' });
await page.evaluate(() => document.fonts.ready);

await page.pdf({
  path: 'font-stable.pdf',
  waitForFonts: true,
  displayHeaderFooter: true,
  headerTemplate,
  footerTemplate,
  margin: { top: '25mm', bottom: '22mm' },
});

The waitForFonts option was introduced in Puppeteer 22.13.0. Check the installed Puppeteer version before depending on version-specific behavior.

7. Preserve print colors when needed

PDF output colors are modified for printing by default. If a header background or brand color must remain exact, apply the documented CSS adjustment:

.pdf-header,
.pdf-footer {
  -webkit-print-color-adjust: exact;
  print-color-adjust: exact;
}

Also pass printBackground: true to include background paint.

8. Troubleshooting checklist

Symptom Likely cause Fix
Header and footer are both missing displayHeaderFooter is omitted or false. Set displayHeaderFooter: true.
Only the header is missing No headerTemplate, invalid template HTML, or insufficient top margin. Add a minimal template, validate its markup, and increase margin.top.
Only the footer is missing No footerTemplate, invalid markup, or insufficient bottom margin. Add a minimal template and increase margin.bottom.
Footer overlaps the last paragraph Bottom margin is shorter than the footer or print CSS changes content height. Increase margin.bottom; inspect @media print.
Header is clipped at the top Template height exceeds the top margin. Increase margin.top or reduce template padding and font size.
Page numbers show blank values Classes are misspelled or placed outside the template markup. Use exactly pageNumber and totalPages.
PDF looks different from the browser Print media styles are active. Inspect print CSS or call emulateMediaType('screen').
Page size is wrong Format and CSS @page rules disagree. Set format explicitly or use preferCSSPageSize: true.
Custom font changes pagination Fonts were not ready when layout was measured. Keep waitForFonts: true, bring the page to front, and await document.fonts.ready.
Colors disappear Background printing is disabled or print color adjustment changes colors. Set printBackground: true and use -webkit-print-color-adjust: exact.
Template CSS has no effect The template does not inherit the page stylesheet. Put required styles inline in the template.

9. Reduce the problem to a minimal reproduction

When the output remains unexplained, record:

  • Installed Puppeteer version and browser version.
  • The complete PDFOptions object.
  • The smallest HTML document that reproduces the issue.
  • All relevant @page and @media print rules.
  • Whether screen media, custom fonts, fixed elements, or page breaks are involved.

Start with plain text in both templates, explicit margins, and a standard format. Add styles and dynamic values one at a time. This separates an option problem from an application stylesheet or browser rendering interaction.

10. Performance, reliability, and cost considerations

  • Keep templates small. Header and footer fragments should contain only the markup and styles they need.
  • Wait for the right readiness condition. networkidle0, a specific selector, and document.fonts.ready solve different timing problems; choose the condition that matches the page.
  • Make paper geometry explicit. Explicit format and margins reduce environment-dependent pagination.
  • Control CSS deliberately. Print rules, fixed positioning, and font loading can change page count and therefore footer values.
  • Capture diagnostics. Save the HTML, PDF options, and browser versions with failed artifacts so a later comparison is reproducible.

Or skip the browser setup

ScreenshotNeo provides a website capture API when you need an image or PDF without maintaining Puppeteer. Use the API documentation at screenshotneo.com/docs for the available options.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://stripe.com \
  -o shot.webp

Python

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

Node.js

const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://stripe.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const bytes = new Uint8Array(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));

ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and the response reports the result through X-Page-Verdict and X-Billed headers. Its MCP server lets Claude, Cursor, and other MCP clients use take_screenshot, get_page_info, and capture_pdf. The Free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

FAQ

Keep templates self-contained and test image loading in the PDF environment. Inline styles and simple markup make failures easier to isolate.

Font metrics change line wrapping and page count. Wait for fonts before calling page.pdf() and use stable font files.

Should I use format or @page size?

Use format for a standard paper preset. Use CSS @page with preferCSSPageSize: true when the document stylesheet owns the paper dimensions.

What is the fastest first test?

Generate a PDF with a plain-text header, plain-text footer, displayHeaderFooter: true, explicit top and bottom margins, and format: 'A4'. Add styling only after that works.