Join the waitlist

"Page X of Y" in Puppeteer and Playwright PDFs, and why the footer looks wrong

Both Puppeteer and Playwright can stamp page numbers on a PDF. Almost everyone's first attempt comes out microscopic, unstyled or missing entirely. Here's why, and how to fix it.

The basic version

page.pdf() takes a footerTemplate (and headerTemplate). Chrome fills any element with these class names:

Class Value
pageNumber current page
totalPages total pages
date print date
title document title
url document URL
await page.pdf({
  path: 'out.pdf',
  format: 'A4',
  displayHeaderFooter: true,
  headerTemplate: '<span></span>',   // an empty header, or Chrome prints its default one
  footerTemplate: `
    <div style="font-size:10px; width:100%; text-align:center; color:#555;">
      Page <span class="pageNumber"></span> of <span class="totalPages"></span>
    </div>`,
  margin: { top: '20mm', bottom: '20mm' },
});

Why it goes wrong

  1. It's invisible. Header and footer templates render in their own context with a tiny default font size: without an inline size, the footer text in my test came out under 1pt. Always set font-size inline.
  2. It prints on top of your content. The template draws in the page margin area, and Chrome doesn't move the body out of its way. With margin: 0, the footer lands over your last line of text. Give top and bottom real space.
  3. Your CSS doesn't apply. The template doesn't see your page's stylesheets. Inline every style.
  4. Images and fonts don't load. The template won't fetch external resources. Embed a logo as a base64 data URI and stick to system fonts.
  5. You get a default header you didn't ask for. With displayHeaderFooter: true, leaving out headerTemplate prints Chrome's default (date and title). Pass an empty element.

The limits

The template is the same on every page. You can't show "Chapter 3" in the header only on chapter 3's pages, number pages differently in an appendix, skip the footer on a cover page, or use your document's own fonts. That's what CSS paged media (@page margin boxes and named strings) is for, and Chrome doesn't support it by itself. Paged.js is the usual workaround; see the next post.


I'm building Galley PDF, an HTML-to-PDF API with page numbering and running headers that use your own fonts and CSS. Join the waitlist.