The journal · 19 July 2026 · 8 min read
HTML to PDF margins, headers and footers: how to control them
A header that only shows on page one, a footer that overlaps the content, page numbers that will not appear: margins and running headers are the second most common HTML to PDF headache after page breaks. Here is the CSS that sets margins and the two reliable ways to get a header and footer that repeat on every page.
You get the content right, the page breaks land cleanly, and then two smaller things spoil the PDF: the margins are wrong, and the header or footer you added shows up on page one and nowhere else. Or the footer sits on top of the last line of a table because nothing reserved space for it. Margins and running headers and footers are the second most common HTML to PDF frustration after page breaks, and like page breaks they are controllable once you know which layer owns them. Some of it is CSS, and some of it belongs to the rendering engine, and mixing the two up is what causes most of the pain.
The short version
Page margins in HTML to PDF are set with the CSS @page { margin: ... } rule, or with the margin parameter your rendering engine or API exposes. A header or footer that repeats on every page is a separate job: in a browser-based renderer you either use the engine's own header and footer templates (Chromium's headerTemplate and footerTemplate, which give you page numbers and dates), or you use a position: fixed element in your HTML, which Chromium paints into the page margin on every page. The classic mistake is putting a normal <div> footer at the bottom of the document and expecting it on every page: it appears once, because in the document flow it only exists once.
Setting the page margins
The margin is the whitespace between the paper edge and your content. Set it once with the @page rule, which is the CSS built specifically for paged output. You can give one value for all four sides or the usual top, right, bottom, left shorthand, and you should use physical print units like millimeters or inches rather than pixels, because the output is a physical page.
@page {
size: Letter;
margin: 18mm 16mm; /* 18mm top and bottom, 16mm left and right */
}
A few practical numbers: for a business document, 15mm to 20mm all round reads well and leaves room for a header and footer. Go much tighter than 12mm and content starts to feel cramped against the edge, and some office printers cannot print into the outer few millimeters anyway. If you are rendering through an API, it usually exposes the same control as a margin parameter so you do not have to inject CSS, which is handy when the HTML is not yours to edit. One caveat worth knowing: when you turn on the engine's own header and footer templates, described next, the top and bottom margins are what make room for them, so those two things are linked.
The two reliable ways to repeat a header and footer
There are exactly two approaches that work in a Chromium-based renderer, and they suit different needs. Pick based on whether you need automatic page numbers.
Option one: the engine's header and footer templates. Chromium's PDF output accepts a small HTML template for a header and another for a footer, and it repeats them on every page automatically. Their real value is that they expose the running data you cannot compute in your own markup: the current page number, the total page count, the date and the document title. You opt in by enabling the display of headers and footers, then supply the template HTML.
// Puppeteer
await page.pdf({
format: "Letter",
displayHeaderFooter: true,
margin: { top: "22mm", bottom: "18mm" },
headerTemplate: `<div style="font-size:9px; width:100%; text-align:center;">
Monthly Statement
</div>`,
footerTemplate: `<div style="font-size:9px; width:100%; text-align:center;">
Page <span class="pageNumber"></span> of <span class="totalPages"></span>
</div>`,
});
Two things bite people here every single time. The first is that the template font size defaults to tiny and inherits none of your page styles, so if you do not set font-size inline the header can appear as a nearly invisible sliver. The second is spacing: the header and footer are drawn inside the page margin, so if your top and bottom margins are too small they overlap the body. Give yourself a top margin of at least 20mm when a header is on. The special classes Chromium substitutes are pageNumber, totalPages, date, title and url, and they only work inside these templates, not in your normal document body.
Option two: a fixed-position element in your HTML. If you do not need page numbers and you want the header or footer to look exactly like the rest of your design, put a position: fixed element in the page. Chromium repeats any fixed element on every printed page, painting it in the same spot each time, so a fixed footer bar with your company details or a fixed header with a logo appears throughout. This keeps everything in your own CSS, so fonts, colors and a logo image all match your brand with no separate template to style.
@media print {
.doc-header { position: fixed; top: 0; left: 0; right: 0; height: 16mm; }
.doc-footer { position: fixed; bottom: 0; left: 0; right: 0; height: 12mm; }
/* reserve room so body content never slides under them */
body { margin-top: 20mm; margin-bottom: 16mm; }
}
The trade off is that this route gives you no automatic page numbering, because your HTML has no way to know which page a given render lands on. So the rule is simple: need "Page 3 of 12"? Use the engine templates. Want a branded bar that matches your design and no page numbers? Use a fixed element. You can even combine them, a fixed branded header plus an engine footer template for the page count.
Why your footer only shows on the last page
This is the number one support question on the topic, and the cause is always the same. A plain <footer> or <div> placed at the end of your HTML is a single element that exists once in the document flow. When the content is short it sits at the bottom of page one; when the content is long it flows to wherever the document ends, which is the last page. It is not "the footer of every page", it is one element at one position. To get a footer on every page you must use one of the two repeating mechanisms above. There is no CSS that turns an ordinary in-flow element into a per-page footer; the running footer has to live either in the engine template or in a fixed-position element.
Expense reports and invoices are where this shows up most, because those documents run long and legal or accounting teams want a page count and a document id on every page. If your generated PDFs are expense reports, the running footer is often carrying the same reference number your expense management software uses to reconcile the report downstream, so it is worth getting the repeat right rather than stamping it on page one only.
Margins, headers and footers through an API
None of this changes when you render server side through a browser-based API, because the same Chromium print path is doing the work. You set the margin as a parameter, and you get the same header and footer template behavior. That means you can tune the layout in your local Chrome print preview, confirm the margins and the running footer look right, and expect the API output to match.
curl https://api.sitepdf.com/v1/render \
-H "Authorization: Bearer $SITEPDF_KEY" \
-F url=https://app.example.com/statements/8842 \
-F format=Letter \
-F margin_top=22mm \
-F margin_bottom=18mm \
-F print_background=true \
-F archive=true
Because the engine is a real, current browser, your @page margins, fixed headers and footer templates all behave the way the print preview showed, which is exactly why teams move off older converters when the layout details start to matter. The full parameter list is on the PDF generator API page, and if you are wrestling with where pages split as well, the controlling HTML to PDF page breaks guide is the companion to this one. For the basics of converting a whole page, convert HTML to PDF covers the starting point.
A quick checklist
| Symptom | Fix |
|---|---|
| Margins wrong or in pixels | Set @page { margin: 18mm 16mm; } or the margin parameter, in mm or inches |
| Footer only on the last page | Use the engine footer template or a position: fixed footer, not a plain div |
| Need "Page X of Y" | Enable header and footer display and use pageNumber and totalPages |
| Header nearly invisible | Set an inline font-size in the template; it inherits nothing |
| Header or footer overlaps the body | Increase the top and bottom margins to make room |
| Branded header must match design | Use a position: fixed element in your own CSS |
Set the margins with @page, pick the repeating mechanism that matches whether you need page numbers, and preview it in Chrome's print dialog before you generate a single PDF. Get those two decisions right and the header, footer and margins fall into place on every page, not just the first.
Written by the team building Sitepdf, an HTML to PDF API that archives every page it renders. The in-browser converter is free to try; early access locks the launch pricing.