What Are Page Margin Boxes?
Page margin boxes are special CSS-generated regions that live in the margins of a printed page. They let you place repeating content — page numbers, document titles, company logos, chapter names — in the header, footer, and side margins of every page, using nothing but CSS.
Until late 2024, browsers ignored the @page margin box at-rules defined in the CSS Paged Media Module Level 3 specification. Developers had to rely on JavaScript polyfills like Paged.js or accept the browser's built-in header/footer (the ones that print the URL and date). That changed with Chrome 131 (released November 2024) and Safari 18.2, which shipped native support for all 16 margin box regions.
The Page Box Model
Every printed page is represented by a page box. The page box has two main areas:
- Page content area — where your HTML document content flows (text, images, tables, etc.).
- Page margin area — the space around the content area defined by the
@pagemarginproperty. This margin area is subdivided into 16 margin box regions.
Each margin box region is an independent CSS box. You target it with a specific at-rule nested inside @page, and you must set the content property for the box to be generated. Without content, the margin box does not exist.
Replacing Browser-Generated Headers & Footers
Browsers traditionally print a default header (page title, URL) and footer (date, page number) on every page. When you define any margin box in your CSS, browsers that support margin boxes suppress their default header and footer entirely. Your margin boxes replace them — they do not stack on top of the defaults.
If you want to suppress the default browser header/footer but don't want any custom content, you can declare an empty margin box:
@top-center { content: ""; }.
Browser Support at a Glance
| Browser | Version | Margin Boxes | Page Counters |
|---|---|---|---|
| Chrome | 131+ (Nov 2024) | Yes | Yes |
| Edge | 131+ (Chromium) | Yes | Yes |
| Opera | 117+ (Chromium) | Yes | Yes |
| Brave | 1.73+ (Chromium) | Yes | Yes |
| Safari | 18.2+ (Dec 2024) | Yes | Yes |
| Firefox | — | No | No |
All Chromium-based browsers (Chrome, Edge, Opera, Brave, Arc, Vivaldi) share the same rendering engine, so margin box support landed across all of them simultaneously. Firefox has tracked this feature in Bug 1854974 but has not shipped it as of early 2026. If you generate PDFs server-side with a Chromium-based tool like Doppio or Puppeteer, margin boxes work reliably today.
The 16 Margin Box Regions
The page margin is divided into 16 non-overlapping rectangular regions. Each region has a fixed position and a corresponding at-rule name. Here is a complete diagram:
┌──────────────────────────────────────────────────────────────┐
│ @top-left-corner │ @top-left │ @top-center │ @top-right │ @top-right-corner │
├───────────────────┼───────────────┴───────────────┴──────────────┼───────────────────┤
│ │ │ │
│ @left-top │ │ @right-top │
│ │ │ │
│ │ │ │
│ @left-middle │ PAGE CONTENT AREA │ @right-middle │
│ │ │ │
│ │ │ │
│ @left-bottom │ │ @right-bottom │
│ │ │ │
├───────────────────┼──────────────┬────────────────┬──────────────┼───────────────────┤
│ @bottom-left-corner│ @bottom-left │ @bottom-center │ @bottom-right│@bottom-right-corner│
└───────────────────┴──────────────┴────────────────┴──────────────┴───────────────────┘
Top Margin Boxes (5 regions)
| At-rule | Position | Common use |
|---|---|---|
@top-left-corner |
Top-left corner (intersection of top and left margins) | Logo, decorative element |
@top-left |
Top margin, left-aligned | Document title, chapter name |
@top-center |
Top margin, centered | Section title, running header |
@top-right |
Top margin, right-aligned | Date, page number |
@top-right-corner |
Top-right corner (intersection of top and right margins) | Logo, decorative element |
Left Margin Boxes (3 regions)
| At-rule | Position | Common use |
|---|---|---|
@left-top |
Left margin, top-aligned | Rarely used; decorative sidebar content |
@left-middle |
Left margin, vertically centered | Vertical text, watermark |
@left-bottom |
Left margin, bottom-aligned | Rarely used |
Right Margin Boxes (3 regions)
| At-rule | Position | Common use |
|---|---|---|
@right-top |
Right margin, top-aligned | Rarely used |
@right-middle |
Right margin, vertically centered | Vertical text, watermark |
@right-bottom |
Right margin, bottom-aligned | Rarely used |
Bottom Margin Boxes (5 regions)
| At-rule | Position | Common use |
|---|---|---|
@bottom-left-corner |
Bottom-left corner | Decorative element |
@bottom-left |
Bottom margin, left-aligned | Copyright notice, confidentiality |
@bottom-center |
Bottom margin, centered | Page numbers — the most popular margin box |
@bottom-right |
Bottom margin, right-aligned | Date, version number |
@bottom-right-corner |
Bottom-right corner | Decorative element |
Syntax
Margin boxes are declared as nested at-rules inside @page. The general pattern is:
@page {
margin: 2cm;
@top-center {
content: "My Document Title";
}
@bottom-center {
content: counter(page);
}
}
The content Property Is Required
A margin box only exists if its content property is set. This mirrors how ::before and ::after pseudo-elements work. If you omit content, the box is never generated — no space is reserved, no styling is applied.
@page {
/* This box will NOT appear — no content property */
@top-left {
font-size: 10pt;
color: gray;
}
/* This box WILL appear */
@top-right {
content: "Page " counter(page);
font-size: 10pt;
color: gray;
}
}
The content property accepts the same values as pseudo-element content:
- Strings —
content: "Chapter 1"; - Counters —
content: counter(page); - Images —
content: url("logo.png"); - Concatenation —
content: "Page " counter(page) " of " counter(pages); - Attr() — not commonly used in margin boxes, but valid per spec.
- Empty string —
content: "";generates the box but shows nothing (useful to suppress defaults).
Styling Margin Boxes
Margin boxes accept a subset of CSS properties. The most commonly used ones are:
| Property | Notes |
|---|---|
content |
Required. Defines what appears in the box. |
font-family, font-size, font-weight, font-style |
Typography for the content. |
color |
Text color. |
text-align |
Overrides the default alignment of the region. |
vertical-align |
Vertical alignment within the margin box. |
border, border-bottom, etc. |
Useful for separator lines between header/footer and content. |
padding |
Internal spacing within the margin box. |
background, background-color |
Background fill or image. |
width, height |
Override auto-sizing; be cautious with overflow. |
Here is a fully-styled example that creates a header with a bottom border and a gray, centered footer:
@page {
margin: 2.5cm 2cm;
@top-left {
content: "Quarterly Report — Q4 2025";
font-family: "Inter", sans-serif;
font-size: 9pt;
color: #555;
border-bottom: 0.5pt solid #ccc;
padding-bottom: 4pt;
vertical-align: bottom;
}
@top-right {
content: "Confidential";
font-family: "Inter", sans-serif;
font-size: 9pt;
font-weight: bold;
color: #c00;
border-bottom: 0.5pt solid #ccc;
padding-bottom: 4pt;
vertical-align: bottom;
}
@bottom-center {
content: "— " counter(page) " —";
font-family: "Inter", sans-serif;
font-size: 8pt;
color: #999;
}
}
Page Counters
CSS Paged Media defines two automatic counters that the browser maintains as it paginates your document:
| Counter | Description |
|---|---|
counter(page) |
The current page number. Starts at 1 and increments automatically for each new page. |
counter(pages) |
The total number of pages in the document. This is resolved after the entire document is paginated. |
"Page X" — Basic Page Number
@page {
@bottom-center {
content: counter(page);
font-size: 10pt;
}
}
"Page X of Y" Pattern
The most popular page numbering format. Combines both counters:
@page {
@bottom-center {
content: "Page " counter(page) " of " counter(pages);
font-size: 10pt;
color: #666;
}
}
This produces output like "Page 3 of 12" on every page.
Counter Styles
The counter() function accepts an optional second argument — a list-style-type value — that changes the numbering format:
| Syntax | Output example | Use case |
|---|---|---|
counter(page) |
1, 2, 3 … | Default decimal numbering |
counter(page, lower-roman) |
i, ii, iii, iv … | Front matter (preface, table of contents) |
counter(page, upper-roman) |
I, II, III, IV … | Formal front matter |
counter(page, lower-alpha) |
a, b, c … | Appendices |
counter(page, upper-alpha) |
A, B, C … | Appendix sections |
counter(page, decimal-leading-zero) |
01, 02, 03 … | Technical manuals |
Example — Roman numeral page numbers for the front matter of a book:
@page frontmatter {
@bottom-center {
content: counter(page, lower-roman);
font-size: 10pt;
font-style: italic;
color: #666;
}
}
.preface, .toc {
page: frontmatter;
}
Starting the Counter at a Custom Value
The page counter starts at 1 by default, but you can reset it on any element using the standard counter-reset property. This is useful when the first few pages (cover, table of contents) shouldn't be counted:
/* Reset page counter to 0 at the start of the main content.
The first page of .main-content will be page 1. */
.main-content {
counter-reset: page;
}
/* Or start at a specific number */
.appendix {
counter-reset: page 100;
}
Note:
counter-reset: page;resets to 0, and the next page increments it to 1. To start at page 5, usecounter-reset: page 4;.
Practical Examples
Below are six complete, copy-paste-ready examples that cover the most common use cases.
1. Simple Footer with Page Numbers
The minimal setup — a centered page number at the bottom of every page:
@page {
size: A4;
margin: 20mm;
@bottom-center {
content: counter(page);
font-family: system-ui, sans-serif;
font-size: 9pt;
color: #888;
}
}
2. Header with Document Title + Page Numbers
A common pattern for reports and white papers — title on the left, page number on the right:
@page {
size: A4;
margin: 25mm 20mm;
@top-left {
content: "Annual Performance Review 2025";
font-family: "Inter", sans-serif;
font-size: 9pt;
color: #444;
vertical-align: bottom;
padding-bottom: 6pt;
border-bottom: 0.5pt solid #ddd;
}
@top-right {
content: "Page " counter(page) " / " counter(pages);
font-family: "Inter", sans-serif;
font-size: 9pt;
color: #444;
vertical-align: bottom;
padding-bottom: 6pt;
border-bottom: 0.5pt solid #ddd;
}
}
3. Different Headers on Left & Right Pages
For book-style layouts, use the :left and :right page pseudo-classes to mirror content. Left (even) pages show the book title; right (odd) pages show the chapter title. Page numbers are pushed to the outer edge:
@page :left {
margin: 20mm 25mm 20mm 30mm;
@top-left {
content: counter(page);
font-size: 9pt;
color: #666;
}
@top-right {
content: "Design Systems Handbook";
font-size: 9pt;
font-style: italic;
color: #666;
}
}
@page :right {
margin: 20mm 30mm 20mm 25mm;
@top-left {
content: "Chapter 3: Color Theory";
font-size: 9pt;
font-style: italic;
color: #666;
}
@top-right {
content: counter(page);
font-size: 9pt;
color: #666;
}
}
4. First Page with No Header, Subsequent Pages with Header
Suppress the header on the first page using :first:
@page {
margin: 25mm 20mm;
@top-center {
content: "Technical Specification v2.4";
font-size: 9pt;
color: #555;
border-bottom: 0.5pt solid #ccc;
padding-bottom: 4pt;
vertical-align: bottom;
}
@bottom-center {
content: counter(page);
font-size: 9pt;
color: #888;
}
}
@page :first {
@top-center {
content: normal;
}
}
Setting content: normal (or omitting the content property entirely in an overriding rule) prevents the margin box from being generated on the first page.
5. Company Logo in Footer
Use content: url() to place an image in a margin box. Combine it with text in other regions:
@page {
size: letter;
margin: 20mm;
@bottom-left {
content: url("/images/company-logo.svg");
vertical-align: middle;
}
@bottom-center {
content: "© 2025 Acme Corp. All rights reserved.";
font-size: 7pt;
color: #999;
vertical-align: middle;
}
@bottom-right {
content: counter(page) " / " counter(pages);
font-size: 8pt;
color: #666;
vertical-align: middle;
}
}
Tip: Use SVG images for logos — they scale crisply at any print resolution. If you use a raster image, ensure it is at least 300 DPI for print quality.
6. Full Invoice Template
A production-ready invoice layout with a header, footer, page numbers, and branded styling:
@page invoice {
size: A4;
margin: 25mm 20mm 30mm 20mm;
@top-left {
content: url("/images/logo.svg");
vertical-align: middle;
}
@top-center {
content: "INVOICE";
font-family: "Inter", sans-serif;
font-size: 14pt;
font-weight: 700;
letter-spacing: 0.15em;
color: #1a1a2e;
vertical-align: middle;
}
@top-right {
content: "INV-2025-0042";
font-family: "Inter", sans-serif;
font-size: 9pt;
color: #666;
vertical-align: middle;
}
@bottom-left {
content: "Acme Corp — 123 Business Ave, Suite 400, New York, NY 10001";
font-family: "Inter", sans-serif;
font-size: 7pt;
color: #999;
vertical-align: middle;
}
@bottom-right {
content: "Page " counter(page) " of " counter(pages);
font-family: "Inter", sans-serif;
font-size: 8pt;
color: #666;
vertical-align: middle;
}
}
.invoice-document {
page: invoice;
}
With this CSS, every element with the class invoice-document will be rendered on pages that carry the invoice header, address footer, and page numbering — all without a single line of JavaScript.
Interaction with Named Pages
Named pages (covered in depth in Named Pages & Selectors) combine powerfully with margin boxes. You can define completely different headers, footers, and numbering for each named page type.
Cover Page with No Numbers, Body Pages with Numbers
/* Cover page: no margin boxes at all */
@page cover {
margin: 0;
/* No margin box declarations = no header/footer */
}
/* Body pages: full header and footer */
@page body {
margin: 25mm 20mm;
@top-left {
content: "Product Catalog 2025";
font-size: 9pt;
color: #444;
}
@top-right {
content: counter(page);
font-size: 9pt;
color: #444;
}
@bottom-center {
content: "www.example.com";
font-size: 7pt;
color: #aaa;
}
}
/* Assign pages */
.cover {
page: cover;
}
.catalog-content {
page: body;
counter-reset: page;
}
In this setup, the cover page renders edge-to-edge with no margin boxes, while the catalog content pages display headers and footer. The counter-reset: page on .catalog-content ensures page numbering starts at 1 after the cover.
Different Margin Boxes per Section
You can define as many named pages as you need. A typical book structure might look like:
@page frontmatter {
@bottom-center {
content: counter(page, lower-roman);
font-size: 9pt;
color: #888;
}
}
@page chapter {
@top-left {
content: "Chapter Title Here";
font-size: 9pt;
color: #555;
}
@bottom-right {
content: counter(page);
font-size: 9pt;
color: #555;
}
}
@page appendix {
@top-center {
content: "Appendix";
font-size: 9pt;
font-weight: bold;
color: #555;
}
@bottom-center {
content: "A-" counter(page);
font-size: 9pt;
color: #555;
}
}
.toc { page: frontmatter; }
.chapter { page: chapter; counter-reset: page; }
.appendix { page: appendix; counter-reset: page; }
Browser Support — Detailed Breakdown
Here is a comprehensive compatibility table for every feature discussed on this page.
| Feature | Chrome 131+ | Safari 18.2+ | Firefox |
|---|---|---|---|
Margin box at-rules (@top-center, etc.) |
Yes | Yes | No |
counter(page) |
Yes | Yes | No |
counter(pages) (total page count) |
Yes | Yes | No |
counter(page, lower-roman) etc. |
Yes | Yes | No |
content: url() in margin boxes |
Yes | Yes | No |
@page :first with margin boxes |
Yes | Yes | No |
@page :left / :right with margin boxes |
Yes | Yes | No |
| Named pages with margin boxes | Yes | Yes | No |
| Borders & backgrounds in margin boxes | Yes | Partial | No |
counter-reset: page |
Yes | Yes | No |
Chromium-based browsers — Edge, Opera, Brave, Arc, and Vivaldi — all inherit Chrome's Blink engine and therefore support margin boxes from the same version onward. If you target Chromium for PDF generation (which tools like Doppio, Puppeteer, and Playwright do), you get full margin box support.
Safari shipped margin box support in Safari 18.2 (December 2024) on macOS and iOS. Some advanced styling properties (complex backgrounds, gradients) may render differently from Chromium; test your layout in both engines if cross-browser print support is critical.
Firefox does not support margin boxes or counter(page)/counter(pages) in any release as of March 2026. The feature is tracked in Mozilla Bug 1854974. If you need Firefox compatibility, consider using Paged.js as a polyfill.
Tips and Gotchas
1. The content property must be set
This is the single most common mistake. If you declare a margin box without content, nothing will appear:
/* ❌ WRONG — no content property, box is not generated */
@page {
@bottom-center {
font-size: 10pt;
color: red;
}
}
/* ✅ CORRECT */
@page {
@bottom-center {
content: counter(page);
font-size: 10pt;
color: red;
}
}
2. Margin boxes replace browser defaults
When you use any margin box, browsers that support them will completely remove their default printed headers and footers (URL, date, page count). This is generally what you want, but be aware that if you only set @bottom-center, the top of the page will be completely blank — the browser does not fall back to its default header.
3. Margin size determines margin box space
The physical space available for margin boxes is determined by the @page { margin: ... } value. A margin: 10mm gives you only 10mm of vertical space for top/bottom margin boxes. If your content is taller than the margin, it will be clipped or overflow unpredictably.
/* Generous margins give room for elaborate headers/footers */
@page {
margin: 30mm 20mm;
@top-left {
content: url("logo.svg"); /* Logo needs height — ensure 30mm is enough */
vertical-align: middle;
}
}
/* Tight margins constrain margin box content */
@page {
margin: 10mm;
@top-center {
content: "Short text only"; /* 10mm ≈ 28pt — keep font small */
font-size: 7pt;
}
}
4. Margin box sizing and overflow
Each margin box is auto-sized to share available space with its neighbors in the same row. For example, @top-left, @top-center, and @top-right divide the top margin's horizontal space into three boxes. If you set a fixed width on one, the others adjust accordingly. Content that overflows a margin box is clipped by default — there is no scrolling in print.
5. No HTML content in margin boxes
Margin boxes can only contain CSS-generated content (strings, counters, images via url()). You cannot place HTML elements or use JavaScript to populate them. For truly dynamic content (like pulling a chapter title from the DOM), you will need a tool like Paged.js or a server-side templating approach that injects values directly into the CSS.
6. The string-set property (future)
The CSS Paged Media specification defines a string-set property that lets you capture element content and use it in margin boxes with string(). This would allow patterns like:
/* ⚠️ NOT YET SUPPORTED in any browser as of March 2026 */
h2 {
string-set: chapter-title content();
}
@page {
@top-left {
content: string(chapter-title);
}
}
This property is not supported in Chrome, Safari, or Firefox yet. Paged.js does polyfill it. Watch for future browser releases.
7. Enable background printing
Browsers disable background colors and images by default when printing. If your margin boxes use background or background-color, users (or your PDF tool) must enable the "print backgrounds" option. In Chromium's headless PDF generation, pass printBackground: true. In Doppio's API, this is enabled via the printBackground parameter.
8. Specificity and cascade in @page rules
When multiple @page rules match the same page, the standard CSS cascade applies:
@page(generic) has the lowest specificity.@page :first,@page :left,@page :righthave higher specificity.@page myname(named) has the same specificity as pseudo-class selectors.@page myname:firsthas the highest specificity.
If you want to suppress a margin box on a specific page, override it with content: normal in a more specific rule:
@page {
@top-center {
content: "My Report";
}
}
@page :first {
@top-center {
content: normal; /* No header on first page */
}
}
9. Testing margin boxes
You cannot see margin boxes in a regular browser window — they only appear when printing or generating a PDF. To test during development:
- Chrome DevTools — Open DevTools → More Tools → Rendering → Emulate CSS media type → select "print". Note: this shows print styles but does not render margin boxes in the viewport.
- Print Preview — Use Ctrl+P / Cmd+P to open the browser print dialog. Margin boxes appear in the preview.
- Save as PDF — The most reliable test. Save to PDF and open the file to verify your margin boxes render correctly.
- Doppio API — If you are generating PDFs programmatically, use the Doppio API to render your page and inspect the resulting PDF.
10. Concatenating multiple values in content
You can combine strings, counters, and images in a single content declaration by separating values with spaces (no commas):
@page {
@bottom-left {
content: "Draft — printed " counter(page) " of " counter(pages);
}
@bottom-right {
content: "v2.1 — " url("icon-print.svg");
}
}
Quick Reference
A compact cheat-sheet of margin box syntax:
@page {
margin: 25mm 20mm;
/* ===== TOP ROW ===== */
@top-left-corner { content: ""; }
@top-left { content: "Document Title"; }
@top-center { content: "Section Name"; }
@top-right { content: "Page " counter(page); }
@top-right-corner { content: ""; }
/* ===== LEFT COLUMN ===== */
@left-top { content: ""; }
@left-middle { content: ""; }
@left-bottom { content: ""; }
/* ===== RIGHT COLUMN ===== */
@right-top { content: ""; }
@right-middle { content: ""; }
@right-bottom { content: ""; }
/* ===== BOTTOM ROW ===== */
@bottom-left-corner { content: ""; }
@bottom-left { content: "© 2025 Company"; }
@bottom-center { content: counter(page) " / " counter(pages); }
@bottom-right { content: "Confidential"; }
@bottom-right-corner{ content: ""; }
}
/* Page pseudo-classes */
@page :first { @top-center { content: normal; } }
@page :left { @top-left { content: counter(page); } }
@page :right { @top-right { content: counter(page); } }
/* Named page with custom margin boxes */
@page chapter { @top-left { content: "Chapter Title"; } }
.chapter { page: chapter; }
/* Counter styles */
counter(page) /* 1, 2, 3 */
counter(page, lower-roman) /* i, ii, iii */
counter(page, upper-roman) /* I, II, III */
counter(page, lower-alpha) /* a, b, c */
counter(page, decimal-leading-zero) /* 01, 02, 03 */
/* Reset counter */
.section { counter-reset: page; } /* restarts at 1 */
.section { counter-reset: page 4; } /* next page = 5 */