← dompdf.js Studio

@page Paged Media in dompdf.js: Headers, Footers, and Page Numbers

Almost every formal PDF needs headers, footers, and page numbers: the company name, the document title, the page number, the total page count, the confidentiality level. Inserting these manually on every page is tedious and error-prone, but dompdf.js configures them centrally through @page paged media rules: @page controls page size and margins, margin boxes such as @top-center and @bottom-right hold the header and footer content, and counter(page) with counter(pages) produce the current page number and the total count. One configuration applies to every page automatically, and when the document length changes, the numbering updates by itself with no manual maintenance. This article starts with the basic @page syntax, covers size and margin in detail, explains the six margin box positions and their uses, and digs into the mechanics of page counting. A complete code example demonstrates a formal document with headers, footers, and page numbers, followed by page-break control and orphan and widow handling, and closes with a troubleshooting checklist for the problems that come up most often, so you can get the page-level details of your PDFs right the first time.The guide also covers the practical decisions that determine whether a document looks professionally finished: how much margin to leave for binding, which of the six margin box positions to use for which content, how to format page numbers for prefaces versus body text, and how to control where chapters break without creating blank pages. The examples are complete enough to adapt directly to your own templates, and the troubleshooting list doubles as a pre-release checklist for any document that needs headers, footers, or page numbers. Once these rules are in place, page-level polish stops being manual work and becomes configuration.

The @page Rule: Size and Margin

@page is the core rule of CSS paged media and describes the page itself. size defines the physical dimensions: size: A4 uses a standard sheet, size: A4 landscape rotates it, and explicit dimensions such as size: 210mm 297mm are also valid. dompdf.js supports the standard paper presets, and the page size determines the physical size of the output file.

margin defines the page margins: @page { margin: 20mm } sets all four sides uniformly, and margin-top, margin-bottom, margin-left, and margin-right set them individually. The margins bound the content area, where both body content and header and footer material live, so their values directly control how much content fits on every page.

Understanding the size and margin relationship matters: the content area height equals the page height minus the top and bottom margins, which directly determines how many pages a long document occupies. Margins below roughly 15mm make pages feel crowded, and generous margins waste paper; 15-25mm suits body text, and binding scenarios deserve extra margin on the binding side, matching print standards.

Page size choice is a product decision more than a technical one: A4 dominates business documents outside North America, Letter dominates inside it, and A3 suits posters and large-format tables. The size determines the content area and therefore how many pages a given amount of content occupies, so switching sizes mid-project changes pagination everywhere. Decide the size once at the start, verify the content area with the target margins, and design the template against that fixed canvas.

Margins deserve the same early decision: they define how much room the header and footer consume, how much content fits per page, and how the document feels. A common error is treating margins as purely decorative, then discovering that the header overlaps the body because the top margin is smaller than the header's height. Reserve the top margin generously, usually 25mm or more when a header with a rule is present, and the header and body coexist cleanly.

Margin Boxes: Containers for Headers and Footers

Margin boxes are special regions defined inside the @page rule, living within the page margins and reserved for header and footer content. The commonly used ones are @top-left, @top-center, and @top-right in the top margin and @bottom-left, @bottom-center, and @bottom-right in the bottom margin, six positions covering the left, center, and right columns of both header and footer.

Margin box content is declared with the content property and accepts text, fonts, and colors: @top-center holds the document title, @bottom-left the company name, and @bottom-right the page number. A margin box only occupies space when it has content, so templates can enable positions independently without one affecting the others.

Styling inside margin boxes mirrors normal elements: font-size, font-weight, and color all apply. Headers and footers conventionally use smaller type, roughly 9-11px, and a lighter color such as #666 to stand apart from body text without competing with it, following the typographic conventions of formal documents.

The six margin boxes give you a complete page furniture system: title and chapter on top, company and page number on the bottom, with left, center, and right columns on each edge. The convention is to keep the header sparse, one element per page, typically a centered title or a left title with a right chapter name, and to use the footer for the operational details, page numbers and confidentiality notices. Resist filling every box; empty space is part of the design.

Margin box styling is intentionally limited compared to body CSS, which is a feature: headers and footers that try to do too much look cluttered and fight the content. Use small type, a restrained color, and at most a hairline rule to separate the header from the body. If you need a rich footer with logos or multiple columns, that is a signal to reconsider the design rather than to push the margin box syntax further.

Page Numbers and Total Counts

Page numbering comes from counters: counter(page) is the current page number and counter(pages) the total page count, referenced inside the content property of a margin box. content: counter(page) ' / ' counter(pages) outputs 3 / 12 style numbering, and the counter increments automatically during pagination with no JavaScript involved.

The number format is customizable: content: 'Page ' counter(page) prints Page 3, counter(page, upper-roman) prints Roman numerals suitable for prefaces and tables of contents, and named pages combined with separate @page rules can give different sections different numbering schemes, such as separate sequences for body and appendix.

Counters are maintained in render order, so multi-page documents get continuous page numbers and an accurate total. Note that counters are directly available only inside @page margin boxes, not in body content, so page-related dynamic information belongs in margin boxes, which is also the most maintainable place for it.

Page numbering conventions differ by document type: business reports use simple Arabic numerals, prefaces and tables of contents often use Roman numerals, and appendices sometimes restart numbering. With named pages, each section can carry its own @page rule and therefore its own counter presentation, which is how professional publishers achieve these mixed schemes. Plan the numbering scheme before building the template, because retrofitting per-section counters is more work than designing them in.

The total count, counter(pages), has a caveat: it reflects the number of physical pages generated, not logical sections. A cover page, a table of contents, and an appendix all count toward the total, so Page 3 of 12 can be the first page of the body. If the requirement is body-only numbering, the cleanest approach is separating the document into multiple PDFs or accepting physical numbering, and matching the label to what the reader will actually see.

Code Example: Full Header, Footer, and Page Numbers

The example builds a complete page system in one @page rule: an A4 page with generous margins, a centered header with the report title and a hairline rule, a confidential footer on the left, and a Page X of Y counter on the right. The page-break-before on the section forces a clean chapter start, demonstrating how pagination control and margin boxes cooperate. Copy the rule into your own template, change the strings, and the entire document inherits the page furniture instantly.

Notice that the counter syntax appears inside the content property with mixed quoted strings and counter() calls. The quotes around the literal text and the unquoted counter names are both required; swapping them is a common source of the everything-is-blank symptom. Keep the pattern exactly as written and adapt only the text, and the numbering will work on the first render.

After the first successful render, try two variations to deepen your understanding: switch the page size to A5 and watch how the content area shrinks and pagination shifts, then add a second forced break and observe the blank-page behavior described in the troubleshooting section. Both experiments take seconds and teach you the interactions between size, margin, and break control far better than reading about them, and they prepare you to debug real documents with confidence.

import { DomPDF } from 'dompdf.js';

const html = `
  <style>
    @page {
      size: A4;
      margin: 25mm 20mm 22mm 20mm;
      @top-center {
        content: '2026 Annual Business Analysis Report';
        font-size: 10px;
        color: #666;
        border-bottom: 1px solid #ccc;
        padding-bottom: 4px;
      }
      @bottom-left {
        content: 'Cloudpine Technology - Confidential';
        font-size: 9px;
        color: #999;
      }
      @bottom-right {
        content: 'Page ' counter(page) ' of ' counter(pages);
        font-size: 9px;
        color: #999;
      }
    }
    h1 { text-align: center; }
    .section { page-break-before: always; }
  </style>
  <h1>Business Analysis Report</h1>
  <p>Page one: executive summary and key metrics...</p>
  <div class='section'>
    <h2>Chapter 2 - Revenue Structure</h2>
    <p>Content starting on page two, forced by page-break-before...</p>
  </div>`;

const pdf = new DomPDF({ format: 'A4', margin: '20mm' });
pdf.addPage(html, { format: 'A4' });
pdf.save('page-media-demo.pdf');

Pagination Control: page-break and Orphans & Widows

Pagination control properties decide where content breaks between pages: page-break-before: always forces an element to start on a new page, ideal for chapter headings; page-break-after: always forces a break after an element; and page-break-inside: avoid prevents an element from being split across pages, the right tool for cards and table rows that must stay whole.

Orphan and widow control keeps paragraphs typographically correct: orphans specifies the minimum number of lines that must remain at the bottom of a page when a paragraph breaks, widows the minimum at the top, both defaulting to 2. Setting orphans: 3; widows: 3 on body paragraphs prevents lonely single lines at page edges and gives the document a polished look.

Understand the precedence and scope of break control: break-inside: avoid acts within the element, while break-before and break-after act at its boundaries. When nested rules conflict, the outer container's keep-together request wins, so long inner content may be pushed entirely to the next page, creating large blank areas; layer the rules and balance completeness against space utilization.

Page-break control is a balancing act between completeness and space: forcing every section onto a fresh page guarantees clean chapter starts but wastes paper, while letting everything flow produces denser documents with less predictable structure. The conventional compromise is to force breaks only for top-level sections and let subsections flow naturally. Set the policy once in the template so every future section inherits the same behavior.

The keep-together properties have a known interaction with long tables and card lists: a table row with break-inside: avoid that is taller than the remaining space forces itself to the next page, potentially leaving a large gap. When that happens, either allow the row to break or increase the available space by reducing the number of rows per page. Testing one realistic long document catches these interactions before they reach production.

Common Problems and Debugging Tips

Q: Headers and footers do not show up? A: Verify the @page rule is inside the stylesheet with complete syntax and that the margin box content property is assigned. Remember that nested rules like @top-center must live inside the @page block; placing them outside invalidates the whole rule.

Q: Page numbers show 0 or do not increment? A: Check the counter spelling and quoting: in content: counter(page), page is a keyword and must not be quoted. For multi-page documents, add pages in order since counters follow render order, and out-of-order addPage calls scramble the numbering.

Q: Content overflows into the margins? A: Check whether margin box content is too long; an overly wide header gets truncated. Shorten the text, reduce the header font size, or show only the title without the subtitle to keep the margin boxes tidy.

Q: Forced page breaks produce blank pages? A: Check whether page-break-after: always on one element combines with page-break-before: always on the next; two stacked forced breaks create an empty page. Remove one of them, or standardize on the break-before family for all pagination control.

Blank pages after forced breaks are the most-reported pagination bug, and the cause is almost always double forcing: an element with page-break-after: always followed immediately by an element with page-break-before: always. Remove one of the two declarations, or standardize on break-before for every section, and the phantom pages disappear. A quick scan of the template for paired always values usually finds the culprit in seconds.

For debugging page furniture, the fastest technique is a minimal document: one page with a single heading and a paragraph, the full @page rule, and nothing else. If the header and page number appear there, the rule is sound and the problem lives in the real content, such as an unclosed tag or a malformed nested rule. This binary search between template and content turns a frustrating hunt into a two-minute check.

⚡ 现场演示(点击生成 PDF)

下面的按钮用 dompdf.js 在浏览器端实时生成 PDF,无需后端:

Hello from dompdf.js!

这是由 dompdf.js 渲染的示例 PDF 内容。