PDF Page Numbering: A Multi-Language Developer's Guide
You're probably at the annoying end of a PDF pipeline right now. The document renders, the fonts look right, the tables finally stop splitting in the middle of rows, and then page numbers turn into the last thing blocking release.
That's normal. PDF page numbering looks simple until you need it to survive real output conditions: repeated headers, mixed page sizes, first-page exceptions, “Page X of Y”, or accessibility rules where the visible page number has to match what assistive tech announces. Manual fixes don't scale, especially in server-side rendering flows.
The part that trips teams up is that there isn't one universal approach. Different stacks expose page numbers in different places: print templates during render, CSS counters in paged layout, placeholder tokens in converter-specific options, or direct text stamping after the PDF already exists. Choosing the right path upfront saves a lot of rework.
The Four Paths to Programmatic PDF Page Numbering
If you've only seen page numbering tutorials built around manual PDF editors, you've seen the wrong layer for most production systems. The core problem usually sits earlier in the pipeline, where HTML becomes PDF and numbers need to appear automatically on every build. That gap matters because manual correction after generation often isn't realistic in batch workflows, and 78% of developers using headless browsers report inconsistent page breaks and footer alignment issues according to the problem context referenced alongside Adobe's page renumbering documentation at Adobe's page renumbering guide.
Page numbering methods at a glance
| Method | Best For | Flexibility | Library Support |
|---|---|---|---|
| Header and footer templates | Fast setup in browser-based renderers | Moderate | Strong in JavaScript, C#, Python wrappers |
| CSS paged media | Print-focused layouts and custom styling | High | Depends heavily on renderer |
| Library-specific placeholders | Existing converter-based stacks | Moderate | Good where the converter exposes tokens |
| Post-processing stamps | Existing PDFs or locked generation pipelines | Very high | Broad across languages |
The practical split is simple:
- Use templates when you want the shortest path to “Page X of Y”.
- Use CSS paged media when layout control matters more than convenience.
- Use placeholders when your converter already speaks that syntax.
- Use post-processing when you can't change rendering, or you're assembling PDFs from multiple sources.
A lot of finance and reporting teams also care about navigation after the PDF lands downstream. If you're trying to streamline review workflows, especially in document-heavy tax or audit contexts, it's worth looking at resources that streamline tax review with PDF tools because page numbering usually sits next to bookmarks, sections, and reviewer navigation.
For broader stack selection, this roundup of PDF makers for HTML conversion workflows is useful when you're deciding whether you want browser rendering, converter-based output, or a stamping-first flow.
Practical rule: Pick the numbering method that lives closest to the place your pages are created. Every layer you add after that increases failure modes.
Method 1 Using Header and Footer Templates
Header and footer templates are the fastest route when your renderer supports them natively. You pass a small HTML fragment for the top or bottom margin area, and the renderer injects special page-number fields at print time. That avoids editing the body HTML just to get numbers onto every page.
This works well for standard reports, invoices, statements, and exports where the footer position is fixed and the numbering format is simple.

JavaScript example
In JavaScript, the common pattern is enabling header and footer rendering, then providing a footer template with the built-in page placeholders.
Detailed Puppeteer PDF guidance is useful if your issue is broader than numbering, but the core pattern looks like this:
const browser = await launchBrowser();
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle0' });
await page.pdf({
path: 'output.pdf',
format: 'A4',
displayHeaderFooter: true,
margin: {
top: '20mm',
bottom: '18mm',
left: '12mm',
right: '12mm'
},
headerTemplate: `<div></div>`,
footerTemplate: `
<div style="width:100%; font-size:9px; padding:0 12mm; color:#555; text-align:center;">
Page <span class="pageNumber"></span> of <span class="totalPages"></span>
</div>
`,
printBackground: true
});
await browser.close();
The important part isn't the HTML. It's the margins. If the bottom margin is too small, the footer either overlaps content or gets clipped.
C# example
The same idea carries over in C# wrappers around browser rendering.
await using var browser = await LaunchBrowserAsync();
await using var page = await browser.NewPageAsync();
await page.SetContentAsync(html);
await page.PdfAsync("output.pdf", new PdfOptions
{
Format = PaperFormat.A4,
DisplayHeaderFooter = true,
PrintBackground = true,
MarginOptions = new MarginOptions
{
Top = "20mm",
Bottom = "18mm",
Left = "12mm",
Right = "12mm"
},
HeaderTemplate = "<div></div>",
FooterTemplate = @"
<div style='width:100%; font-size:9px; padding:0 12mm; color:#555; text-align:center;'>
Page <span class='pageNumber'></span> of <span class='totalPages'></span>
</div>"
});
Python example
Python wrappers usually expose nearly identical options.
browser = await launch_browser()
page = await browser.newPage()
await page.setContent(html, waitUntil='networkidle0')
await page.pdf({
'path': 'output.pdf',
'format': 'A4',
'displayHeaderFooter': True,
'printBackground': True,
'margin': {
'top': '20mm',
'bottom': '18mm',
'left': '12mm',
'right': '12mm'
},
'headerTemplate': '<div></div>',
'footerTemplate': """
<div style="width:100%; font-size:9px; padding:0 12mm; color:#555; text-align:center;">
Page <span class="pageNumber"></span> of <span class="totalPages"></span>
</div>
"""
})
await browser.close()
Where templates work well and where they don't
Templates are strong when you need:
- Consistent footer placement across every page.
- Minimal implementation time in browser-based PDF rendering.
- Simple totals such as “Page X of Y”.
They start to break down when you need:
- Different numbering by section
- Book-style left and right page layouts
- Tight visual integration with body content
- Advanced first-page or front-matter rules
Don't try to force complex print design into template footers. Once you need section-aware numbering or different margin boxes by page type, move to paged CSS or post-processing.
Method 2 Gaining Control with CSS Paged Media
Template footers are convenient, but they're also boxed in. CSS paged media gives you more control because numbering becomes part of the print layout itself instead of a separate header or footer fragment injected by the renderer.
That matters when the footer needs to feel like it belongs to the page design, not like a bolt-on.

Basic counter-based numbering
A typical pattern uses @page and a page counter. Renderer support varies, so always test with your exact engine before you commit to this approach in production.
<html>
<head>
<style>
@page {
size: A4;
margin: 20mm 12mm 18mm 12mm;
@bottom-center {
content: "Page " counter(page);
font-size: 9pt;
color: #555;
}
}
body {
font-family: sans-serif;
}
</style>
</head>
<body>
<main>
<!-- long content -->
</main>
</body>
</html>
You can also add totals if your renderer supports counter(pages):
@page {
@bottom-center {
content: "Page " counter(page) " of " counter(pages);
}
}
Better styling and section variants
Paged CSS becomes useful when you need print-specific variation:
First page exceptions
@page:first { @bottom-center { content: ""; } }Left and right pages
@page:left { @bottom-left { content: counter(page); } } @page:right { @bottom-right { content: counter(page); } }Prefixed numbering
@page { @bottom-center { content: "Appendix " counter(page); } }
If footer spacing feels off, the underlying issue is often page margins versus body spacing. For a quick refresher on that distinction, this explainer on understanding CSS margin and padding is worth revisiting because print layouts magnify those mistakes.
Trade-offs in real projects
Paged media is usually the cleanest design solution, but not always the easiest engineering solution. Support can vary a lot between rendering engines, which means the same CSS may produce different output depending on the runtime. That's why I treat this as a layout-first approach, not a universal default.
This Java-focused HTML to PDF guide is useful if your stack relies on a Java-side renderer, because CSS paged media support often becomes a selection criterion, not just an implementation detail.
If your numbering disappears entirely, don't start by debugging CSS syntax. Check whether your renderer supports the margin-box features you're using. Unsupported paged CSS often fails silently.
Method 3 Leveraging Library-Specific Placeholders
Some PDF generators don't use HTML header templates or paged CSS as their primary interface. Instead, they expose page numbering through placeholders in command arguments or library options. In those stacks, forcing a browser-style solution usually creates more friction than value.
This approach is practical when your converter already supports built-in page variables and your requirements are ordinary: current page, total pages, maybe a left, center, or right footer.

Typical placeholder pattern
A placeholder-based footer often looks like this:
converter input.html output.pdf \
--margin-bottom 18mm \
--footer-center "Page [page] of [topage]"
The exact option names differ by library, but the idea is stable. The converter replaces [page] with the current page index and [topage] with the final total during generation.
That same pattern usually maps into wrapper libraries:
options = {
"margin-bottom": "18mm",
"footer-center": "Page [page] of [topage]"
}
render_pdf(html, "output.pdf", options=options)
var options = new Dictionary<string, string>
{
["margin-bottom"] = "18mm",
["footer-center"] = "Page [page] of [topage]"
};
RenderPdf(html, "output.pdf", options);
When placeholders are the right fit
Placeholder-based numbering works best when:
| Situation | Fit |
|---|---|
| Existing converter in production | Strong |
| Need “Page X of Y” only | Strong |
| Need highly custom visual layout | Weak |
| Need section restarts or mixed numeral systems | Weak to moderate |
The upside is operational simplicity. You don't need custom footer HTML, and you often don't need to modify document templates at all. The downside is limited control. Once product asks for a cover page with no number, Roman numerals in front matter, and Arabic numbers after the table of contents, placeholder systems start feeling cramped.
If you're choosing a library family rather than just implementing a footer, this overview of HTML to PDF library options helps frame where placeholder-driven renderers fit compared with browser-based and CSS-heavy paths.
A mistake I see often is trying to use placeholders for branding-heavy layouts. They're better treated as infrastructure features, not full document-design systems.
Method 4 Stamping Numbers with Post-Processing
Post-processing is the escape hatch when you can't touch the HTML, can't change the render step, or you're starting from a PDF that already exists. You generate the document first, then reopen it and draw numbers onto each page.
It's the most flexible option, and also the one with the highest implementation overhead.
Core workflow
The stamping flow is always some version of this:
- Generate or receive a PDF with no page numbers.
- Load the PDF in a manipulation library.
- Read the page count.
- Iterate over pages.
- Draw text at coordinates based on each page's width and height.
- Save a new PDF.
JavaScript example
const pdfDoc = await loadPdfBytes(inputBytes);
const pages = pdfDoc.getPages();
const total = pages.length;
pages.forEach((page, index) => {
const { width } = page.getSize();
const text = `Page ${index + 1} of ${total}`;
page.drawText(text, {
x: width / 2 - 30,
y: 20,
size: 9
});
});
const output = await pdfDoc.save();
C# example
using var document = LoadPdf("input.pdf");
int total = document.Pages.Count;
for (int i = 0; i < total; i++)
{
var page = document.Pages[i];
string text = $"Page {i + 1} of {total}";
DrawText(page, text, x: page.Width / 2 - 30, y: 20, fontSize: 9);
}
document.Save("output.pdf");
Python example
document = load_pdf("input.pdf")
total = len(document.pages)
for index, page in enumerate(document.pages):
text = f"Page {index + 1} of {total}"
draw_text(page, text, x=page.width / 2 - 30, y=20, font_size=9)
document.save("output.pdf")
The trade-offs that matter
Post-processing is the right answer when:
- The PDF comes from a third-party system
- Multiple PDFs get merged before numbering
- You need absolute placement control
- You need to add numbers after review or assembly
It's a weaker fit when throughput matters, because it adds another read-write cycle and another library layer to your pipeline. It also raises coordination issues with annotations, forms, signatures, and incremental updates.
If your PDFs also need fields or overlays beyond numbering, this guide on adding form fields to PDF workflows complements the same post-processing model.
Stamping is usually the most reliable fix for inherited PDFs. It's rarely the cleanest architecture for PDFs you already control at the HTML rendering layer.
Troubleshooting Common PDF Numbering Errors
The visible symptom usually isn't the root cause. “Page 1 of 1” on every page, missing totals, clipped footers, and accessibility failures each point to a different layer in the pipeline.
When totals are wrong
If every page shows the same total or the same current page, the renderer may not be resolving page variables at final layout time. That's common when numbering is attempted too early, before pagination stabilizes, or in environments where the footer gets rendered as regular HTML instead of a print-aware footer context.
Check these first:
- Render context. Make sure numbering lives in a print footer or margin-box feature, not the body.
- Late-loading assets. Images and web fonts can change pagination after you think layout is done.
- Merged documents. If files are joined after page numbering, totals become stale.
When the footer overlaps content
This is usually not a numbering bug. It's a box-model bug.
Look at:
- Bottom page margin being smaller than the footer area
- Body content using absolute positioning into the print margin
- Fixed elements colliding with footer space
A good test is to temporarily make the footer background visible. If the footer is rendering but sitting on top of the content stream, the page reserved too little room for it.
When numbers don't appear at all
If your CSS footer rules are ignored, assume renderer support before assuming syntax problems. If your template footer is blank, verify that header and footer output is explicitly enabled and that the document margins leave enough printable space.
Plainly put, the same footer markup that works in one renderer may be a no-op in another.
The accessibility issue people miss
For compliance-sensitive documents, visible numbering isn't enough. The W3C's PDF17 technique requires that the page number announced by screen readers match the number printed on the page, and it defines consistent page numbering as part of accessible PDF behavior in the W3C PDF17 technique.
That's where many automated flows fall short. They draw a footer that says “iii” or “12”, but the PDF's internal page labels still expose a different sequence. The document looks correct and still fails an accessibility audit.
A footer is just paint unless the PDF's internal page labels agree with it.
If you need Roman numerals in front matter and Arabic numbering in the main body, test both the visual footer and the PDF's page labels in a viewer that exposes document page labels, not just physical page positions.
Frequently Asked Questions
How do I add Roman numerals to the front matter and Arabic numbers to the main content
Use a method that supports section-aware numbering or post-processing with page-label control. Simple footer templates and placeholder systems can draw i, ii, iii visually, but they may not update the PDF's internal page labels. If accessibility matters, treat visual numbering and internal page labels as separate tasks.
Can I restart page numbering for each chapter
Yes, but the implementation depends on the method. CSS paged media can handle section-oriented layout if your renderer supports the needed features. Post-processing can also restart displayed numbers page by page. Header and footer templates are usually the weakest option for chapter restarts unless your generation flow renders each chapter independently.
What's the most reliable way to create Page X of Y
For browser-style HTML to PDF generation, header and footer templates are usually the shortest path. For converter-driven stacks, placeholder variables are often simpler. For inherited PDFs, stamping is the most reliable fallback because you already know the final page count before drawing anything.
How do I skip numbering on the cover page
There are three common patterns:
- Template-based. Render an empty first-page footer if the engine supports page-specific templates.
- CSS-based. Use a first-page paged rule that outputs no content.
- Post-processing. Start stamping from page two.
Do FDA and regulated submission workflows care about page numbering consistency
Yes. The FDA's PDF specifications state that navigation is easier when the document page numbers and the PDF file page numbers match, with the initial page numbered as page one, and subsequent split files should continue numbering consecutively in the same submission package according to the FDA PDF specifications. In regulated workflows, numbering isn't cosmetic. It affects review usability and submission quality.
What about accessibility standards for PDF page numbering
For accessibility-sensitive documents, align the visible footer, the PDF navigation labels, and the logical numbering scheme from the start. W3C formally established PDF17 as a WCAG 2.0 technique in 2011 for consistent PDF page numbering, including the requirement that announced page numbers match visible page numbers, documented in the W3C PDF17 working draft.
If you're building HTML-to-PDF pipelines and need deeper implementation patterns across JavaScript, C#, and Python, Transformy's guides are a practical place to continue, especially if you're deciding between rendering-time numbering and post-processing in a production workflow.