Skip to content
CastPDF

CSS print page numbers: Page X of Y in your PDF

To print page numbers with CSS, put content: counter(page) inside an @page margin box such as @bottom-right, and use counter(pages) for the total. Hide the number on the cover with @page :first. Then render with a paged media engine, as our HTML to PDF API does in print mode.

  • Updated October 2026
  • 100 free PDFs a month

The problem: HTML has no pages to count

Your client wants a twelve-page service agreement with "Page 4 of 12" at the bottom of each sheet. On screen the document is one long scroll. There is no page four yet, so there is nothing in the HTML you can number.

Most people first try a footer <div> with position: fixed. It does appear on every printed page in many engines. But it cannot count, so it shows the same text everywhere. Others add numbers by hand, which breaks the moment one clause grows by a line and pushes everything down.

Page numbers matter more than they seem. Lawyers cite them, auditors check them, and readers use the total to know nothing is missing. A report without them looks unfinished, even when everything else is right.

Why page numbers need a paged media engine

A page number only exists after the content has been split into pages. CSS has a standard for this, called Paged Media. It gives each page a set of margin boxes, such as @top-left, @bottom-center and @bottom-right, and two counters: page for the current page and pages for the total.

Support for these rules arrived late in browsers, and it is still uneven. That is why tools built on a browser often add their own header and footer system instead. A paged media engine such as Paged.js fills the gap: it lays the pages out in JavaScript first, then fills each margin box with the right numbers.

  • Margin boxes live in the page margin. If the bottom margin is 10mm, a footer with two lines of text has nowhere to go. Size the margin for the content you put in it.
  • The total is only known at the end. counter(pages) needs the whole document laid out before the first footer can be filled. A paged media engine handles this for you.
  • Features differ by engine. Running headers taken from your content (string-set) and page references (target-counter) need full paged media support, not just a print stylesheet.

Add Page X of Y, step by step

  1. Reserve room in the margin. Give the @page rule a bottom margin large enough for the footer, for example 25mm, so the number does not crowd the body text.
  2. Place the counters in a margin box. Inside @page, add @bottom-right with content: "Page " counter(page) " of " counter(pages). Style the text right there, with a font size and colour.
  3. Hide the number on the cover. Add an @page :first rule that sets the same margin box to content: none. The cover stays clean, and page two still reads "Page 2".
  4. Render in print mode. Send "mode": "print" with raw HTML. Saved templates already render in print mode unless you change their settings.
  5. Check the total. Read the x-pages response header and compare it with the last footer in the PDF. They should match.

This stylesheet numbers a service agreement. The page number sits on the right, a document reference sits on the left, and the cover has neither:

agreement-print.css
@page {
  size: A4;
  margin: 22mm 18mm 28mm;
  @bottom-left {
    content: "Agreement SA-2026-118";
    font-size: 8pt;
    color: #94a3b8;
  }
  @bottom-right {
    content: "Page " counter(page) " of " counter(pages);
    font-size: 8pt;
    color: #475569;
  }
}
@page :first {
  @bottom-left { content: none; }
  @bottom-right { content: none; }
}
.cover { break-after: page; }

Put the stylesheet in the css field and the markup in html. The request below is shortened, but every field in it is real:

agreement-request.json
{
  "html": "<section class=\"cover\"><h1>Service Agreement</h1><p>Northwind Studio and Halden Logistics</p></section><h2>1. Scope</h2><p>The supplier provides monthly maintenance.</p><h2>2. Fees</h2><p>Fees are invoiced quarterly.</p>",
  "css": "@page { margin: 22mm 18mm 28mm; @bottom-right { content: \"Page \" counter(page) \" of \" counter(pages); font-size: 8pt; } } @page :first { @bottom-right { content: none; } } .cover { break-after: page; }",
  "mode": "print",
  "format": "A4",
  "filename": "service-agreement.pdf"
}
curl
curl https://api.castpdf.com/v1/pdf \
  -H "Authorization: Bearer $CASTPDF_API_KEY" \
  -H "Content-Type: application/json" \
  -d @agreement-request.json \
  --dump-header headers.txt \
  --output service-agreement.pdf

grep -i "^x-pages" headers.txt

The cover counts as page one even though it shows no number. So the first footer you see reads "Page 2 of 2" here, and that is correct: the reader holds two sheets.

Running headers, contents pages and other variations

A running header with the current chapter

Long documents often show the chapter name at the top of each page. CSS can copy text from your content into the margin with string-set. You name a string on the element that holds the text, then print it with string() in a margin box. The value updates on each page where a new chapter starts.

handbook-header.css
h1.chapter {
  string-set: chapter content(text);
  break-before: page;
}
@page {
  margin: 26mm 20mm 24mm;
  @top-center {
    content: string(chapter);
    font-size: 8.5pt;
    letter-spacing: 0.04em;
    text-transform: uppercase;
  }
  @bottom-center {
    content: counter(page);
    font-size: 9pt;
  }
}

Page numbers in a table of contents

A contents page needs the page where each chapter lands, which you cannot know before layout. The target-counter function looks it up for you. Link each entry to the chapter heading by its id, and the engine prints the page number after the link text.

handbook-contents.html
<style>
  .contents a { display: flex; color: inherit; text-decoration: none; }
  .contents a::after {
    content: target-counter(attr(href), page);
    margin-left: auto;
  }
</style>
<nav class="contents">
  <a href="#leave">Annual leave</a>
  <a href="#expenses">Travel and expenses</a>
  <a href="#equipment">Equipment</a>
</nav>
<h1 class="chapter" id="leave">Annual leave</h1>
<h1 class="chapter" id="expenses">Travel and expenses</h1>
<h1 class="chapter" id="equipment">Equipment</h1>

Fast mode and page numbers

CastPDF has two render modes. Fast mode uses the browser print engine directly and skips the pagination step. In fast mode, string-set and target-counter have no effect. Our page counter tests run in print mode, so send "mode": "print" whenever a document needs numbers, running headers or a contents page.

The Puppeteer footer template

If you run Chrome yourself through Puppeteer, you may use its footer template instead of CSS. It fills elements that have the classes pageNumber and totalPages:

footer-only.mjs
import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setContent('<h1>Service Agreement</h1><p>Clause text.</p>');
await page.pdf({
  path: 'agreement.pdf',
  format: 'A4',
  displayHeaderFooter: true,
  headerTemplate: '<span></span>',
  footerTemplate: '<div style="font-size: 9px; width: 100%; text-align: right; padding-right: 15mm;">Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>',
  margin: { top: '15mm', bottom: '20mm' },
});
await browser.close();

It works, but the footer lives outside your HTML and needs its own inline styles. It also cannot read chapter titles from the page. The Puppeteer header and footer guide covers its options and the usual surprises.

How CastPDF handles it

In print mode, CastPDF runs Paged.js inside Chromium before it prints. Paged.js supports the @page margin boxes, the page and pages counters, and named strings. Footers with Page X of Y, running headers with string-set and contents pages with target-counter are each covered by the renderer test suite.

When you set no margins, pages get 20mm on every side. That fits a single small line of footer text, but larger footers need more. Set margins in the request, in the template settings, or in your own @page rule, which wins over both.

Numbers count pages correctly only when pages break where you expect, so the page break guide is a good companion. For long tables, add a repeating table header. If a footer uses a brand font, read fonts in HTML to PDF first. The full rule list is in the print CSS reference.

FAQ

Common questions

How do I show Page X of Y in a PDF footer with CSS?

Add a margin box such as @bottom-right inside your @page rule. Set its content to "Page " counter(page) " of " counter(pages). The engine must support CSS paged media, as CastPDF print mode does.

How do I hide the page number on the first page?

Add an @page :first rule and set the margin box that holds the number to content: none. The cover still counts as page one, so the next page shows 2.

Why is my margin box footer not showing?

The most common cause is a page margin too small for the text. Another is a render mode without paged media support. Increase the bottom margin and render in print mode.

Can a PDF header show the current chapter title?

Yes, with string-set. Set string-set: chapter content(text) on the chapter heading, then use content: string(chapter) in a top margin box. It needs CastPDF print mode.

How do I put page numbers in a table of contents?

Link each entry to its heading with href="#id". Then use target-counter(attr(href), page) in an ::after rule on the links. The engine fills in the page where each heading lands.

Do CSS page counters work in fast mode?

Running headers and table of contents numbers do not work in fast mode. Page counters are tested in print mode, which is the mode to use for numbered documents. Templates render in print mode by default.

Make your first PDF in 5 minutes

Pick a template, add your details and download your PDF. You get 100 free PDFs every month, and you don’t need a card.