wkhtmltopdf --header-html and --footer-html in Chrome: page numbers, date and logo
If you are moving from wkhtmltopdf to headless Chrome, headers and footers are the part that breaks quietly: the PDF still prints, but the page numbers are missing, microscopic or printed over your last row. This page maps every wkhtmltopdf header and footer variable to what Chrome offers, with code for the three ways to do it. Everything below was printed on Chromium 141 (Linux, 14 October 2026) and read back from the PDF text and positions with pdftotext -bbox; where something is untested it says so.
The variables, side by side
wkhtmltopdf replaces the variables in the manual's list inside --header-left, --footer-right and so on, or fills them into a header HTML file through its URL query string. Chrome has no query string: its header and footer templates (Playwright and Puppeteer headerTemplate and footerTemplate) use empty elements whose class Chrome fills in.
| wkhtmltopdf | Chrome template | What we saw |
|---|---|---|
[page] | <span class="pageNumber"></span> | "Page 2 of 3" on page 2 |
[topage] | <span class="totalPages"></span> | 3 on a 3-page file |
[date], [time] | <span class="date"></span> | Date and time together, in the browser's locale format: "10/14/26, 1:28 AM". For [isodate] or a fixed format, write the text yourself when you build the template string. |
[title], [doctitle] | <span class="title"></span> | The page's <title> |
[webpage] | <span class="url"></span> | The full address; for a local file that is the whole file:/// path, which you rarely want printed |
[frompage], [section], [subsection], [sitepage], [sitepages] | none | No Chrome equivalent. Write fixed text, or print one PDF per section. |
Way 1: CSS page margin boxes (works from the command line)
@page {
size: A4;
margin: 25mm 15mm 20mm;
@top-right { content: "Invoice 42"; font: 9pt Arial, sans-serif; }
@bottom-center { content: "Page " counter(page) " of " counter(pages); font: 9pt Arial, sans-serif; }
}
chrome --headless --no-pdf-header-footer --print-to-pdf=out.pdf file:///path/to/doc.html
This is the closest thing to --footer-center "Page [page] of [topage]" and it is the only way to get a custom header or footer from the plain Chrome command, which has no option for a template. It needs Chrome 131 or newer; on Chromium 141 it printed "Page 1 of 1" at the bottom and "Invoice 42" at the top right. Leave out --no-pdf-header-footer and Chrome adds its own header and footer too: on a 3-page test file it printed the date and title at the top, and the file address and "2/3" at the bottom of page 2.
A logo can go in a margin box with @top-left { content: url("data:image/svg+xml;base64,…"); }. A data: address printed the logo. A relative path to an SVG file (url("logo.svg")) did not print anything in our test, so embed the image.
Way 2: Playwright or Puppeteer header and footer templates
const box = (inner) =>
`<div style="font-size:9px; width:100%; padding:0 15mm;
display:flex; justify-content:space-between">${inner}</div>`;
await page.pdf({
path: "out.pdf",
format: "A4",
displayHeaderFooter: true,
margin: { top: "25mm", bottom: "20mm" },
headerTemplate: box('<span class="title"></span><span class="date"></span>'),
footerTemplate: box('<span>Invoice 42</span><span>Page <span class="pageNumber"></span> of <span class="totalPages"></span></span>')
});
We ran this on a 120-row table. Every page had the title at the left and the date at the right at the top, and "Page 2 of 3" at the bottom right. Three things in that snippet are there because leaving them out went wrong:
- Set a
font-sizein the template. Without one, our header "Quarterly report" measured about 1.3 pt tall: the text is in the PDF but nobody can read it. Withfont-size:9pxthe same words had a line height of about 11 pt, which reads normally. This is the usual cause of "my page numbers are missing". - Set
margintop and bottom. The template is drawn inside the page margin. With no margin set, the body text ran to the bottom edge of the page and the last row of text sat in the same band as the footer (page 2: last body row at 825 pt, footer text at 818 pt, on an 843 pt page). Make the margin at least as tall as the template. - Add side padding. In our test the template started at the very left edge of the page (x = 0), not at your body margin. Padding in the template lines it up.
What will not carry over from a header HTML file
- Scripts do not run. The wkhtmltopdf manual's
--header-htmlexample readspageandtopagefrom the URL with JavaScript. A<script>document.write(...)</script>inside a Chrome footer template printed nothing. Use the class names in the table instead. - Images from a file path do not load. A
file://image in the footer printed as a broken-image icon. Adata:image in the header printed correctly. Embed the logo as a data address. - Your page's CSS does not apply to the template. Put the styles inline in the template, as above.
- There is no
--header-spacing. The distance between the header and the content is set by your top margin and by the template's own height.
Way 3: print from a tool that already does this
The free translator reads a wkhtmltopdf command, for example --footer-center "Page [page] of [topage]" --footer-font-size 8, and prints the margin-box CSS, the Playwright and Puppeteer options and the Chrome command, and lists the variables above that have no equivalent. Related: five page-break patterns for Chrome (repeating table headers, totals that stay together, a cover with no footer).
Limits of this page
Tested: Chromium 141 on Linux with Playwright, and the Chrome command line, using a test file we wrote. Not tested: Windows, macOS, other Chrome versions, Puppeteer itself (its template options match Playwright's), and how wkhtmltopdf itself places its headers, which we describe from its manual. Sizes quoted come from pdftotext -bbox on our own output and will change with your fonts and paper size.
If you generate invoices, quotes, statements or reports and would rather not tune this yourself, Foliokit ($19) is seven finished templates that already carry a "Page X of Y" footer with the margins set, repeating table heads and a cover without a footer, filled from JSON by one command that uses the Chrome you have.