Page numbers and repeating headers belong in the page margin, not in the document body. CSS Paged Media defines margin boxes around every page, plus two counters, page and pages, that resolve while the document is being paginated. Declare them in an @page rule and they appear on every page without touching your markup.
<style>
@page {
size: Letter;
margin: 1in 0.75in;
@top-center {
content: "Acme Corp Q3 Statement";
font-size: 9pt;
color: #555555;
}
@bottom-right {
content: "Page " counter(page) " of " counter(pages);
font-size: 9pt;
color: #555555;
}
}
</style>counter(page) is the current page and counter(pages) is the total, so you get "Page 1 of 3" without knowing the page count in advance. Everything declared inside @page renders once per page, so the header repeats on its own.
Render it with an engine that implements paged media. WeasyPrint does:
from weasyprint import HTML
HTML(filename="report.html").write_pdf("report.pdf")Run that against a document long enough to break across three pages and every page carries the header at top center, with "Page 1 of 3", "Page 2 of 3", and "Page 3 of 3" in the bottom right corner.
Making the header change per section
For a running header that tracks the current section rather than a fixed string, set a named string on the heading and read it back inside the margin box:
<style>
@page {
size: Letter;
margin: 1in 0.75in;
@top-left {
content: string(chapter);
font-size: 9pt;
color: #555555;
}
@bottom-right {
content: "Page " counter(page) " of " counter(pages);
font-size: 9pt;
}
}
h2 { string-set: chapter content(); }
</style>Each page shows the value of the named string as of that page, so a document that runs from "Section one" into "Section two" picks up the new heading text on the page where that heading appears, and keeps it on the pages after it. The body markup does not change at all.
If you render through headless Chrome
Chrome based renderers expose their own header and footer mechanism. In Puppeteer, page.pdf() accepts displayHeaderFooter set to true along with headerTemplate and footerTemplate HTML strings, and injects values into elements carrying the classes date, title, url, pageNumber, and totalPages. Both options default to off, so a template alone does nothing until displayHeaderFooter is set. Check which mechanism your renderer implements before you write the CSS.
Back to All Questions