What Is Paged.js?
Paged.js is an open-source JavaScript library created by the Coko Foundation. It acts as a polyfill for the W3C CSS Paged Media specification, implementing features that browsers have not yet shipped natively.
Paged.js works by intercepting your HTML document in the browser, parsing your CSS for paged-media rules, and then chunking the content into discrete page-sized boxes using JavaScript. The result is a paginated preview you can print or convert to PDF.
Because it runs entirely client-side, Paged.js can be dropped into any HTML document with a single <script> tag. It has been widely adopted by publishers, academic institutions, and book designers who need features like footnotes, running headers derived from content, and cross-reference page numbers—features that historically only commercial tools like PrinceXML offered.
However, browser support for CSS Paged Media has improved dramatically. Chrome 131 (released November 2024) shipped native margin box support, closing one of the largest gaps. This means many projects that previously required Paged.js can now use pure CSS instead.
What Chrome Supports Natively (as of 2025)
Chromium-based browsers—Chrome, Edge, Opera, Brave, and headless renderers like Doppio—now support a substantial portion of the CSS Paged Media specification without any polyfill. Here is what works out of the box:
The @page Rule
Full support for the @page at-rule, including the size descriptor (A4, letter, legal, custom dimensions), margin shorthand and individual margins, and page-orientation (upright, rotate-left, rotate-right).
Page Breaks
The modern fragmentation properties break-before, break-after, and break-inside are fully supported, along with orphans and widows for controlling minimum line counts at page boundaries.
Named Pages
The page property lets you assign elements to named @page rules, enabling different page layouts for different document sections (e.g., landscape tables within a portrait document).
Page Selectors
The pseudo-class selectors :first, :left, :right, and :blank work natively, allowing you to apply different styles to the first page, left-hand pages, right-hand pages, and intentionally blank pages.
Margin Boxes (Chrome 131+)
All 16 margin box regions (@top-left, @top-center, @top-right, @bottom-left-corner, etc.) are supported natively since Chrome 131. You can place arbitrary content—text, images, counters—in the page margin area using pure CSS.
Page Counters
The built-in counter(page) and counter(pages) counters work inside margin boxes, giving you automatic page numbering and total page counts without JavaScript.
In summary: Chrome natively covers page sizing, margins, orientation, breaks, named pages, page selectors, margin boxes, and page counters. For many document types, this is everything you need.
What Still Requires Paged.js
Despite Chrome’s progress, several parts of the CSS Generated Content for Paged Media and related specifications remain unimplemented in any browser. These features still require Paged.js (or a commercial tool like PrinceXML or Weasyprint):
string-set() and string() — Running Headers from Content
The string-set property captures the text content of an element (like a chapter title) into a named string. The string() function then retrieves it inside a margin box. This is how you create running headers that automatically display the current chapter or section name. No browser implements this natively.
h2 { string-set: chapter-title content(); }
@page {
@top-center {
content: string(chapter-title);
}
}
target-counter() — Cross-References
The target-counter() function lets you display the page number where a linked element appears. This is essential for generating “see page X” cross-references and for building tables of contents that show page numbers next to entries.
a.toc-entry::after {
content: target-counter(attr(href), page);
}
@footnote — Footnote Handling
The @footnote area and float: footnote property allow content to be pulled from the document flow and placed at the bottom of the current page as a footnote. This includes automatic footnote numbering with counter(footnote). Browsers do not implement this.
bookmark-level — PDF Bookmarks
The bookmark-level, bookmark-label, and bookmark-state properties generate a navigable bookmark/outline tree in the output PDF. This is important for long documents where readers rely on the PDF sidebar for navigation. No browser supports these properties.
element() — Content Flows
The element() function removes an element from the normal document flow and inserts it into a margin box or another page region. It is used for advanced running headers that include images or complex markup, not just plain text. This remains unimplemented in browsers.
leader() — Table of Contents Dot Leaders
The leader() function generates a repeating pattern (typically dots) that fills the space between a TOC entry and its page number. While you can approximate this with CSS (using flexbox and a pseudo-element with a dotted border), the native leader() function adapts perfectly to variable-width content. No browser supports it.
Feature Comparison Table
The table below compares every major CSS Paged Media feature across native Chrome (131+) and Paged.js. Use it as a quick reference to determine whether your project needs the polyfill.
| Feature | Native Chrome | Paged.js | Notes |
|---|---|---|---|
@page rule |
Yes | Yes | Both fully support the base @page at-rule. |
size descriptor |
Yes | Yes | A4, letter, legal, custom dimensions. Use preferCSSPageSize in headless APIs. |
@page margins |
Yes | Yes | Shorthand and individual margin properties. |
| Page orientation | Yes | Yes | Portrait, landscape, and page-orientation for rotation. |
| Named pages | Yes | Yes | Assign elements to named @page rules via the page property. |
:first page selector |
Yes | Yes | Style the first page differently. |
:left / :right selectors |
Yes | Yes | Alternating styles for recto/verso pages. |
:blank selector |
Yes | Yes | Style intentionally blank pages inserted by break-before: left etc. |
Margin boxes (@top-center, etc.) |
Yes | Yes | Chrome 131+ supports all 16 margin box regions natively. |
counter(page) / counter(pages) |
Yes | Yes | Automatic page numbering in margin boxes. |
break-before / break-after |
Yes | Yes | Force or avoid page breaks. |
break-inside |
Yes | Yes | Prevent elements from splitting across pages. |
orphans / widows |
Yes | Yes | Control minimum lines at top/bottom of pages. |
| Custom counter styles | Yes | Yes | Use @counter-style with counter(page, style). |
marks (crop / cross) |
Partial | Yes | Chrome supports the property but rendering is limited. |
bleed |
Partial | Yes | Works with marks; practical support depends on output pipeline. |
string-set / string() |
No | Yes | Running headers from content. Major gap in native support. |
target-counter() |
No | Yes | Cross-reference page numbers (e.g., “see page 12”). |
@footnote / float: footnote |
No | Partial | Paged.js implements footnotes, but edge cases remain. |
bookmark-level / PDF bookmarks |
No | Partial | Paged.js has experimental support; PrinceXML is more reliable. |
leader() |
No | Yes | Dot leaders for table of contents. Workarounds exist in pure CSS. |
element() function |
No | Partial | Move elements into margin boxes. Paged.js support is limited. |
@prince-pdf annotations |
No | No | PrinceXML-specific; not part of the W3C spec. |
| Nth-page selectors | No | No | Not in any spec. Use named pages for per-section styling. |
When to Use Native CSS Only
For the majority of real-world document generation tasks, native CSS Paged Media is sufficient. If your use case falls into any of the following categories, you likely do not need Paged.js:
- Invoices and receipts — Fixed layout with a company header, line items, totals, and a footer. Page size and margins via
@page, page numbers viacounter(page). - Contracts and agreements — Multi-page text documents with page numbers and a “Page X of Y” footer. Use margin boxes for the footer and
break-inside: avoidto keep signature blocks together. - Simple reports — Data-driven reports with tables, charts, and a consistent header/footer on every page. Margin boxes handle headers and footers natively.
- Certificates and letters — Single-page or few-page documents where content fits a known layout. Named pages let you apply a unique design to the first page.
- Shipping labels and tickets — Custom page sizes with precise dimensions. The
sizedescriptor accepts any width/height pair. - Performance-sensitive pipelines — When generating hundreds or thousands of PDFs per hour, eliminating JavaScript overhead matters. Native CSS is processed directly by the rendering engine with zero extra parsing.
Estimated coverage: 80–90% of real-world document generation use cases can be handled with native CSS alone. If your document has a predictable structure and does not require content-derived running headers, footnotes, or cross-reference page numbers, native CSS is the right choice.
When to Use Paged.js
Paged.js is the right tool when your document requires features that browsers have not implemented. These are typically found in publishing and academic contexts:
- Books and long-form publications — Running headers that display the current chapter title require
string-set()andstring(), which only Paged.js provides. - Academic papers with footnotes — The
@footnotearea andfloat: footnoteautomatically place footnote content at the bottom of the correct page. There is no pure-CSS equivalent. - Table of contents with page numbers — The
target-counter()function resolves the page number of any linked element. Building a TOC with accurate page numbers requires this or a JavaScript alternative. - Documents with cross-references — “See page 42” references need
target-counter(). Without it, you must hard-code page numbers or compute them after rendering. - Complex multi-column layouts — While CSS multi-column works in some contexts, Paged.js offers finer control over column balancing and fragmentation within paged layouts.
- In-browser paginated preview — If you need to show users a live, paginated preview of their document in the browser (not just at print time), Paged.js renders page boxes directly in the DOM.
Using Both Approaches with Doppio
Doppio is a headless Chromium-based PDF generation API. It supports both native CSS Paged Media and Paged.js—you choose the approach that fits your document.
Native CSS Approach
Send HTML with @page rules, margin boxes, and fragmentation properties. Doppio’s renderer processes them natively with no extra configuration:
curl -X POST https://api.doppio.sh/v1/render/pdf/direct \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"page": {
"pdf": {
"printBackground": true,
"preferCSSPageSize": true
},
"goto": {
"url": "https://your-app.com/invoice/123"
}
}
}'
Set preferCSSPageSize: true so your @page { size: A4; } rule controls the PDF dimensions. The printBackground option ensures background colors and images are included.
Paged.js Approach
Include the Paged.js script in your HTML. When rendering through Doppio, add an explicit wait condition so the polyfill has time to finish paginating before the PDF is captured:
<!DOCTYPE html>
<html>
<head>
<style>
@page {
size: A4;
margin: 25mm;
@top-center {
content: string(chapter-title);
}
@bottom-center {
content: counter(page);
}
}
h2 { string-set: chapter-title content(); }
</style>
<script src="https://unpkg.com/pagedjs/dist/paged.polyfill.js"></script>
</head>
<body>
<h2>Chapter 1: Introduction</h2>
<p>Content here...</p>
</body>
</html>
When Doppio loads this page, Paged.js will intercept the CSS, chunk the content into pages, and resolve the string() functions. Doppio then captures the final rendered output as a PDF.
Tip: When using Paged.js with Doppio, set a
waitForSelector,waitForFunction, or a longertimeoutin your API call to ensure Paged.js has finished processing before the PDF is captured. Doppio also supports reusable templates so you can store your HTML/CSS once and merge data into it via the API.
Performance Considerations
Performance matters when generating documents at scale. The two approaches have fundamentally different performance profiles:
Native CSS: Instant Processing
With native CSS Paged Media, the rendering engine applies your @page rules during layout—the same pass that computes element positions and sizes. There is no additional JavaScript execution, no DOM manipulation, and no second rendering pass. The overhead compared to a regular web page render is negligible.
Paged.js: JavaScript Overhead
Paged.js must parse your CSS, identify paged-media rules, remove them from the cascade, measure content, split it into page-sized containers, and re-insert it into the DOM. For a 10-page document, this typically adds 500ms–2s of processing time. For a 100-page book, it can take 5–15 seconds or more depending on content complexity.
Recommendation for High-Volume Generation
If you are generating hundreds or thousands of PDFs per hour (invoices, reports, receipts), native CSS is the clear choice. Every millisecond of rendering time multiplies across your volume. Eliminating Paged.js from the pipeline can reduce per-document render time by 30–70%, directly lowering infrastructure costs.
If you need Paged.js features for a small number of complex documents (e.g., a quarterly report or a book), the JavaScript overhead is acceptable since these are generated infrequently.
Migration Path: Moving from Paged.js to Native CSS
If you are currently using Paged.js, you do not need to migrate all at once. Chrome’s native support has expanded steadily, and you can adopt features incrementally.
Features You Can Drop Paged.js For Today
If you are using Paged.js solely for any of the following, you can switch to native CSS immediately:
@pagerules (size, margins, orientation)- Margin boxes (
@top-left,@bottom-center, etc.) - Page counters (
counter(page),counter(pages)) - Named pages and
:first/:left/:right/:blankselectors break-before,break-after,break-insideorphansandwidows
These are all natively supported in Chrome 131+ and work reliably in Doppio.
Gradual Migration Strategy
- Audit your CSS — Search your stylesheets for Paged.js-dependent features:
string-set,string(),target-counter(),float: footnote,bookmark-level,element(), andleader(). If none are present, you can remove Paged.js entirely. - Test without the polyfill — Remove the Paged.js
<script>tag and generate a test PDF. Compare it against the Paged.js version. In many cases the output will be identical. - Replace running headers with static content — If your running headers show a fixed company name or document title (not a chapter title that changes per page), you can hardcode the content in a margin box:
@top-center { content: "My Company"; }. - Use JavaScript for remaining gaps — For features like TOC page numbers, consider a two-pass approach: render the document, extract page positions with JavaScript, then re-render with the correct numbers injected. This avoids the full Paged.js dependency for a single feature.
- Watch the Chromium roadmap — The Chrome team has been steadily implementing more of the spec.
string-setandtarget-counter()may land in future releases, closing the remaining gaps.
Features That Still Require Paged.js
If your document uses any of the following, keep Paged.js for now:
- Running headers that change based on page content (
string-set/string()) - Cross-reference page numbers (
target-counter()) - Footnotes (
@footnote/float: footnote) - PDF bookmarks (
bookmark-level) - Advanced content flows (
element()) - Native dot leaders (
leader())
Summary
The landscape has shifted. In 2020, Paged.js was essential for almost any serious paged-media work. In 2025, native CSS covers the vast majority of use cases. Here is the decision framework:
- Use native CSS when your document has a predictable structure with static headers/footers, page numbers, and controlled page breaks. This covers invoices, receipts, contracts, reports, certificates, labels, and most business documents.
- Use Paged.js when you need dynamic running headers from content, footnotes, cross-reference page numbers, or a live paginated preview in the browser.
- Use both when different document types in your application have different needs. Doppio supports either approach transparently.
Generate PDFs with Doppio
Whether you use native CSS Paged Media or Paged.js, Doppio’s API renders your HTML into production-ready PDFs. Start generating in minutes with a free account.
Create a Free Account →