CSS Paged Media vs Paged.js

Chrome now supports most of the CSS Paged Media specification natively. This guide compares native browser capabilities against the Paged.js polyfill so you can decide exactly what your project needs.

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:

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:

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 longer timeout in 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:

These are all natively supported in Chrome 131+ and work reliably in Doppio.

Gradual Migration Strategy

  1. Audit your CSS — Search your stylesheets for Paged.js-dependent features: string-set, string(), target-counter(), float: footnote, bookmark-level, element(), and leader(). If none are present, you can remove Paged.js entirely.
  2. 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.
  3. 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"; }.
  4. 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.
  5. Watch the Chromium roadmap — The Chrome team has been steadily implementing more of the spec. string-set and target-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:

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:

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 →