How to Use Custom CSS Counters in Puppeteer PDF Footers
Add page numbers to Puppeteer PDFs with footer templates, or use CSS page-margin counters when your Chromium version supports them.

For dependable page numbers in a Puppeteer PDF, use Puppeteer’s documented footer mechanism: set displayHeaderFooter: true and provide HTML through footerTemplate. Put pageNumber and totalPages classes on the elements where Puppeteer should insert the current page and document total. Reserve room with the PDF bottom margin.
CSS page-margin counters are a separate option. Chrome documents generated content in page margins from Chrome 131, but support depends on the Chromium build your Puppeteer installation actually runs. Use the template method when you need the documented Puppeteer route; use @page margin boxes when you specifically need CSS-controlled placement and have verified the browser version.
This guide covers both mechanisms, complete Node.js examples, relevant PDF settings, rendering checks, troubleshooting, and ways to keep PDF generation reliable. See the Puppeteer PDFOptions documentation and Chrome’s page-margin guide.
1. The reliable default: Puppeteer’s footer template
The footer template is not ordinary page content. It is a separate HTML template that Puppeteer renders in the PDF header/footer area when displayHeaderFooter is enabled. Puppeteer recognizes classes including pageNumber and totalPages, filling them with the current page number and total page count.

Save the following as pdf-footer.mjs. Install Puppeteer in the project with npm install puppeteer, then run node pdf-footer.mjs. Puppeteer’s normal package setup supplies a compatible browser installation; if your deployment selects a separate Chrome binary, make sure to check that binary’s version as well.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.setContent(`
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font: 12pt/1.5 Arial, sans-serif; }
h1 { break-after: avoid; }
p { margin: 0 0 1em; }
</style>
</head>
<body>
<h1>A multi-page example</h1>
${'<p>Repeated sample content for PDF pagination.</p>'.repeat(100)}
</body>
</html>
`, { waitUntil: 'load' });
await page.pdf({
path: 'report.pdf',
format: 'A4',
printBackground: true,
displayHeaderFooter: true,
margin: {
top: '16mm',
right: '14mm',
bottom: '20mm',
left: '14mm',
},
footerTemplate: `
<div style="width:100%; box-sizing:border-box; padding:0 14mm;
font:9px Arial,sans-serif; color:#555; text-align:right;">
<span class="pageNumber"></span>
/
<span class="totalPages"></span>
</div>
`,
});
} finally {
await browser.close();
}
The repeated paragraph is just a way to make the sample span multiple pages. Replace it with your own content. The footer template is deliberately self-contained and uses inline styles: do not assume stylesheets or classes from the document body will style this separate region.
What each setting does
displayHeaderFooterenables both header and footer rendering; its documented default is false.footerTemplatesupplies the footer HTML. The special classes supported for injected values includepageNumberandtotalPages. Other recognized template classes includedate,title, andurl.margin.bottomreserves printable space. Increase it if the footer is clipped or overlaps body text. Keep padding inside the footer aligned with the page’s left and right margins.formatchooses a paper format such as A4 or Letter. If omitted, Puppeteer documents Letter as the default.landscapechanges orientation.printBackgroundincludes background graphics; its default is false. It can affect output size and is unrelated to counters.preferCSSPageSizegives a CSS@pagesize priority overformat,width, orheight. Its documented default is false.pageRangescan restrict output, for example"1-5, 8". The numbering placeholders should be reviewed in the resulting PDF when printing only a range.scaleaccepts values from 0.1 to 2. Scaling changes content dimensions and pagination, so re-check footer clearance after changing it.waitForFontsdefaults to true in the current reference. When custom fonts matter, wait for them to load and check the output before relying on their metrics.timeoutcontrols the PDF operation timeout. Tune it to the workload and infrastructure rather than disabling it by default.
Options and defaults can evolve, so consult the current API reference for the Puppeteer version pinned by your application.
2. CSS counters with @page margin boxes
If your target Chrome supports generated content in page margins, CSS can place counters directly in the bottom margin:

const cssPageFooter = `
@page {
size: A4;
margin: 16mm 14mm 20mm;
@bottom-center {
content: "Page " counter(page) " of " counter(pages);
font: 9pt Arial, sans-serif;
color: #555;
}
}
body { font: 12pt/1.5 Arial, sans-serif; }
`;
await page.setContent(`
<!doctype html>
<html>
<head><style>${cssPageFooter}</style></head>
<body><h1>Report</h1><p>Document content…</p></body>
</html>
`);
await page.pdf({
path: 'css-footer.pdf',
preferCSSPageSize: true,
displayHeaderFooter: false,
});
CSS Paged Media defines the special page counter for the current page and pages for the total. The Chrome guide says generated page-margin content is available from Chrome 131. A specification describes expected semantics; it does not guarantee that every browser version bundled with Puppeteer implements them. Verify your deployment’s browser and inspect its PDFs before making CSS margin boxes a production dependency.
Do not enable a Puppeteer footer and a CSS margin-box footer for the same content unless you intend to print both: they are distinct mechanisms and can result in duplicate page numbers. Also distinguish ordinary CSS counters in document content from the page counters used in margin boxes. A custom counter such as counter(chapter) is not a substitute for Puppeteer’s injected page placeholders and does not automatically become a page number.
3. Choose the mechanism and control pagination
| Need | Use | Check |
|---|---|---|
| Page X of Y in a Puppeteer PDF across deployed versions | displayHeaderFooter and footerTemplate |
Bottom margin, template styling, clipping |
| CSS-defined placement in the print page margin | @page and @bottom-center or another margin box |
Actual Chromium build supports margin generated content |
| Page size declared in CSS | @page { size: … } with preferCSSPageSize: true |
Conflicts with PDF format/width/height settings |
| Screen styles instead of print styles | page.emulateMediaType('screen') before PDF generation |
Print layout and page margins may no longer match expectations |
Page.pdf() uses print media by default. Puppeteer documents calling page.emulateMediaType('screen') first when screen media is desired. That affects the page’s CSS layout; it does not change which footer technique is supported. See Page.pdf().
- Pin the runtime. Record the Puppeteer package version and whether its bundled browser or a separately managed Chrome binary runs in production.
- Choose one footer owner. Use a Puppeteer template for the direct documented placeholders; choose CSS margin boxes when CSS placement is a requirement and your browser supports it.
- Reserve space. Set top and bottom margins in PDF options or CSS. A footer that renders correctly but occupies no space can collide with content.
- Generate representative documents. Include a one-page document, a long document, a page break near the footer, long titles, and any localized or custom-font content.
- Inspect the PDF artifact. Confirm first and last page numbering, total pages, clipping, repeated headers/footers, and that the footer remains outside body text.
- Repeat after upgrades. Browser changes can affect print layout and pagination even when the JavaScript call remains the same.
4. Waiting for content and managing output
Page numbering is calculated from the paginated PDF output, so content readiness and layout stability matter. For a remote page, navigate with a deliberate wait condition such as domcontentloaded or networkidle0 as appropriate, then wait for any app-specific selector or font readiness before calling page.pdf(). Network idle can stall on analytics or long-lived requests; a selector or explicit application-ready signal is often more predictable. Avoid arbitrary long sleeps when the page exposes a readiness condition.
When generating PDFs from HTML under your control, make content deterministic: provide the final data before printing, wait for images and fonts, and avoid late-running scripts that change element heights. Any content change that moves a page break can change both the current page and total pages. In particular, don’t compute and insert page numbers into the body before pagination; use the footer placeholders or the supported page-margin counters.
For larger jobs, reuse a browser process while creating an isolated page per job, and close pages after use. Bound concurrency based on available memory and document size; PDF layout consumes resources in proportion to page complexity, images, fonts, and page count. Set navigation and PDF timeouts, record failures, and close pages in finally blocks. A browser process should be restarted according to your service’s operational policy if it exits or becomes unhealthy; do not assume a successful HTTP response from an upstream page means the PDF is complete.
PDF file size depends on document content, fonts, images, background printing, and output settings. Avoid loading unnecessarily large images and only enable printBackground when the design needs it. If output is costly to regenerate, cache by a stable content version plus relevant rendering options. Do not reuse a cached file after content, CSS, browser version, or page-size settings change.
5. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| No footer appears | displayHeaderFooter is false or omitted |
Set it to true and provide a nonempty footerTemplate. |
| Literal blank placeholders | Wrong class names, malformed template, or CSS mechanism assumed to work in an unsupported browser | Use exact pageNumber/totalPages class names for templates; check the browser version for CSS margin boxes. |
| Footer is cut off | Bottom margin is too small, or template content is too tall | Increase the PDF bottom margin, reduce footer font/padding, and inspect at the target paper size. |
| Footer overlaps body text | Insufficient page margin or a fixed-position footer was placed in the page body | Use the template or page-margin box and reserve margin space; do not position a repeated footer as ordinary body content. |
| Two page numbers show | Both Puppeteer footer and CSS margin box are enabled | Choose one footer mechanism or deliberately position distinct content in each. |
| Page count changes unexpectedly | Font/image loading, print styles, scaling, margins, or content changed pagination | Wait for content and fonts, stabilize print CSS, then re-check margins and scaling. |
| Colors differ from the browser screenshot | page.pdf() uses print media and adjusts colors for printing |
Use print-specific styles; use -webkit-print-color-adjust when exact color rendering is required, per Puppeteer’s PDF notes. |
| PDF generation times out | Slow navigation, unresolved resources, or an unsuitable timeout | Separate navigation waiting from PDF generation, identify stalled resources, and set a timeout suited to the document. |
| CSS page size appears ignored | preferCSSPageSize is false, or explicit format dimensions take precedence |
Set preferCSSPageSize: true when CSS size should win and remove conflicting settings. |
Or skip the browser setup
If your task is to capture a webpage as an image or PDF rather than build a custom Puppeteer document, ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. One GET request can return a clean screenshot or PDF. The call below follows the documented API example; see the ScreenshotNeo docs for API details and options.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://stripe.com',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
- Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
- Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers report the page verdict and billing status.
- An MCP server provides
take_screenshot,get_page_info, andcapture_pdftools for Claude, Cursor, and other MCP clients. - 1,000 screenshots per month are free with no card. Paid plans start at $5 for 3,000 screenshots.
Get 1,000 free screenshots a month with no card.
6. FAQ
Can I use a custom label like “Page 3” instead of “3 / 8”?
Yes. Put fixed text around the injected spans in footerTemplate, or use CSS content around counter(page) in a supported margin box.
Does Puppeteer’s pageNumber class require custom CSS counters?
No. Puppeteer injects the page value into its documented template class. CSS counters are a separate print-layout mechanism.
Will the CSS method work in every Puppeteer install?
Do not assume so. Puppeteer may run different Chromium builds; check the actual browser version and verify a generated PDF. Chrome’s documented threshold for generated margin content is Chrome 131.
Can the footer show the URL and title too?
The PDF template supports recognized classes including url and title, in addition to date and the page placeholders. Keep template markup self-contained.
Does this add page numbers to an existing PDF?
No. Page.pdf() renders a page to PDF. Adding footer numbers to a previously generated PDF is a separate PDF editing workflow.
Sources
- Puppeteer PDFOptions: header/footer options, template classes, margins, page size, ranges, scaling, and defaults.
- Puppeteer Page.pdf(): print media behavior and screen-media override.
- Chrome for Developers: Add content to the margins of web pages when printed using CSS: margin boxes, counters, and Chrome 131 support note.
- W3C CSS Paged Media Module Level 3: page-margin model and counter semantics.


