Paged.js is an open-source library to paginate content in the browser. Based on the W3C specifications, it’s a sort of polyfill for Paged Media and Generated Content for Paged Media CSS modules. The development was launch as an open-source community driven initiative and it’s still experimental. The core team behind paged.js includes Adam Hyde, Julie Blanc, Fred Chasen & Julien Taquet.
Until we have formal accessible documentation for paged.js, here is a list of links for those who would like to start using paged.js:
- If you want to add your 5 cents worth to the discussion or just contact the team, you can jump and discuss with us on our Mattermost channel.
You can also find below the features we are supporting right now. This text is an extract from the Editoria book.
Page rules
The page rules must be set up in the @print
media query.
@media print{
/* write the page rules here */
}
Size
The size of the pages in a book can be defined by either width and height (in inches or millimeters) or a paper size such as A5 or Letter. It must be the same for all the pages in the book and will be inferred only from the root @page
.
@page {
size: A5;
}
# or
@page {
size: 140mm
200mm;
}
Margins
The margin command defines the top, bottom, left, and right areas around the page’s content.
@page {
margin: 1in
2in .5in
2in;
}
Names
Single pages or groups can be named, for instance as “cover” or “backmatter.” Named pages can have their own, more specific, styles and margins, and even different styles from the main rule.
@page
backmatter {
margin: 20mm
30mm;
background: yellow;
}
In HTML, these page groups are defined by adding the page name to a CSS selector.
section.backmatter {
page: backmatter;
}
Page selectors
Blank pages
The blank selector styles pages that have no content, e.g., pages automatically added to make sure a new chapter begins on the desired left or right page.
@page :blank {
@top-left { content: none; }
}
First page and nth page
There are selectors for styling the first page or a specific page, targeted by its number (named n
in the specification).
@page :first {
background: yellow;
}
@page :nth(5) {
margin: 2in;
}
Left and right or recto and verso
Typically, pages across a spread (a pair of pages) have symmetrical margins and are centered on the gutter. If, however, the inner margin needs to be larger or smaller, the selector to style left and right pages can make that change.
@page
:left {
margin-right: 2in;
}
@page
:right {
margin-left: 2in;
}
Margin boxes
The margins of a page are divided into sixteen named boxes, each with its own border, padding, and content area. They’re set within the @page
query. A box is named based on its position: for example, @top-left
, @bottom-right-corner
, or @left-middle
(see all rules). By default, the size is determined by the page area. Margin boxes are typically used to display running headers, running footers, page numbers, and other content more likely to be found in a book than on a website. The content of the box is governed by CSS properties.
To select these margin boxes and add content to them, use the following example:
@page { @top-center { content: "Moby-Dick"; } }
Generated content
CSS counters
css-counter
is a CSS property that lets you count elements within your content. For example, you might want to add a number before each figure caption. To do so, you would reset the counter for the <body>
, increment it any time a caption appears in the content, and display that number in a ::before
pseudo-element.
body {
counter-reset: figureNumber;
}
figcaption {
counter-increment: figureNumber;
}
figcaption::before {
content: counter(figureNumber)
}
Page-based counters
To define page numbers, paged.js uses a CSS counter that gets incremented for each new page.
To insert a page number on a page or retrieve the total number of pages in a document, the W3C proposes a specific counter named page
. The counters declaration must be used within a content
property in the margin-boxes declaration. The following example declares the page number in the bottom-left box:
@page { @bottom-left { content: counter(page); } }
You can also add a bit of text before the page number:
@page {
@bottom-left {
content: "page " counter(page);
}
}
To tally the total number of pages in your document, write this:
@page {
@bottom-left {
content: counter(pages);
}
}
Repeated elements on different pages
Named string
Named strings are used to create running headers and footers: they copy text for reuse in margin boxes.
First, the text content of the element is cloned into a named string using string-set
with a custom identifier (in the code below we call it “title,” but you can name it whatever makes sense as a variable). In the following example, each time a new <h1>
appears in the HTML, the content of the named string gets updated with the text of that <h1>
.
h1 { string-set: title content(text) }
Next, the string()
function copies the value of a named string to the document, via the content
property.
@page {
@bottom-left {
content: string(title)
}
}
Running elements
Running elements are another way to create running headers and footers. Here the content, complete with style and structure, is copied from the text, assigned a custom identifier, and placed inside a margin box. This is useful for formatted text such as a word in italics.
The element’s position
is set:
.title {
position: running(title);
}
Then it is placed into a margin box with the element()
value via the content
property:
@page { @top-center { content: element(title) } }
Controlling text fragmentation with page breaks
Sometimes there is a need to define how content gets divided into pages based on markup. To do so, paged media specifications include break-before
, beak-inside
, and break-after
properties.
break-before
adds a page break before the element; break-after
adds a page break after the element.
Here is the list of options:
break-before: page
pushes the element (and the following content) to the next available page
break-before: right
pushes the element to the next right page
break-before: left
pushes the element to the next left page
break-before: recto
pushes the element to the next recto page
break-before: verso
pushes the element to the next verso page
break-before: avoid
ensures that no page break appears between two specified elements
For example, this sequence will create a page break before each h1
element:
h1 {
break-before: page;
}
This code, in contrast, will push the h1
to the next right page, creating a blank page if needed:
h1 {
break-before: right;
}
This snippet will keep any HTML element that comes after an h1
on the same page as the h1
, moving them both to the next page if necessary.
h1 {
break-after: avoid;
}
The last option is the break-inside
property, which ensures that the element won’t be separated across multiple pages. If you want to be sure that your block quotes will never be divided, write this:
blockquote {
break-inside: avoid;
}
Cross-references
To build items such as an index or a table of contents, the export function has to find the pages on which the relevant elements appear inside the book. To do so, paged media specifications include a target-counter
property.
For cross-references, links are used that target anchors in the book:
<p>see the <a href="#anchor-name">Title of the chapter</a></p>
Later in the book, the chapter title will appear with the anchor, set using an ID
property.
<h1 id="anchor-name">title of the chapter</h1>
The target-counter
property is used in ::before
and ::after
pseudo-elements and set into the content
property. As a page counter, it can include some text:
a::after {
content: ", page "
target-counter(attr(href), page );
}
In the PDF, this code will be rendered as “see title of the chapter, page 12”.
Extending Paged.js
There are several ways to extend the rendering of Paged.js. Selecting the best method will depend on how the code will be called and what it needs to access.
When creating a script or library that is specifically aimed at extending the functionality of paged.js, it is best to use hooks and a handler class.
Paged.js has various points in the parsing of content, transforming of CSS, rendering, and layout of HTML that you can hook into and make changes to before further code is run.
A handler is a JavaScript class that defines functions that are called when a hook in Paged.js is ready to defer to your code. All of the core modules for support of paged media specifications and generated content are implemented as handlers. To create your own handler, you extend this same handler class.
class
MyHandler
extends
Paged.Handler
{
constructor(chunker, polisher, caller) {
super(chunker, polisher, caller);
}
}
The handler also exposes the underlying tools for fragmenting text (Chunker
) and transforming CSS (Polisher
)—see below.
Within this class, you can define methods for each of the hooks, and specify when they will be run in the code. A return
that is asynchronous will delay the next code using await
.
class
MyHandler
extends
Paged.Handler
{
constructor(chunker, polisher, caller) {
super(chunker, polisher, caller);
}
afterPageLayout(pageFragment, page, breakToken) {
console.log(pageFragment, page, breakToken);
}
}
Paged.js contains the following asynchronous hooks:
Chunker
beforeParsed(content
runs on content before it is parsed and given IDsafterParsed(parsed)
runs after the content has been parsed but before rendering has startedbeforePageLayout(page)
runs when a new page has been createdafterPageLayout(pageElement, page, breakToken)
runs after a single page has gone through layout, and allows adjusting the breakTokenafterRendered(pages)
runs after all pages have finished rendering
Polisher
beforeTreeParse(text, sheet)
runs on the text of the style sheetonUrl(urlNode)
runs any time a CSS URL is parsed.onAtPage(atPageNode)
runs any time a CSS @page is parsedonRule(ruleNode) runs
any time a CSS rule is parsedonDeclaration(declarationNode, ruleNode)
runs any time a CSS declaration is parsedonContent(contentNode, declarationNode, ruleNode)
runs any time a CSS content declaration is parsed
Finally, the new handler needs to be registed in order to be used.
Paged.registerHandlers(MyHandler);
This can be registered anytime before the preview has started and will persist through any instances of Paged.Previewer
that are created.
If a JavaScript library, such as MathJax, needs access to the content before it is paginated, you can delay pagination until that script has completed its work. This will give the library full access to the content of the book but has the disadvantage of needing to render the entire book before rendering each page, which can cause a significant delay.
Given that the polyfill will remove the page contents as soon as possible, adding a window.PagedConfig
will allow you to pass a Promise
that will delay until it is resolved.
let promise = new
Promise((resolve, reject) {
someLongTask(resolve);
});
window.PagedConfig = {
before: () => {
return promise;
}
};
It is also possible to delay rendering of the polyfill until called by passing auto: false
.
window.PagedConfig = {
auto: false
};
window.PagedPolyfill.preview();
When the Previewer
class is used directly, the preview()
method can be called at any point that is appropriate.