Skip to content

Pagination

reader.pagination describes what is visible: which spine items and pages are on screen, where they are in the book and whether that position has resolved. Use it for everything the reader sees: which page is being read, how many pages are in a chapter, how far into the book they are. The .pagination reference lists state$, state and every field of a result; this page is about using them.

It is not what to save to reopen the book where the reader left it. That is the reading position, and the next section explains the difference.

Two values describe where the reader is, and they answer different questions:

reader.pagination reader.navigation.readingPosition$
Answers What is on screen now Where the reader is in the book
Moves when The reader navigates, and whenever the book is laid out again: a resize, a rotation, a font size change, a chapter loading nearby The reader navigates, and the page it goes to is laid out. Never on a relayout after that
Use it for Page numbers, progress bars, the current chapter's title Saving progress, reopening the book, syncing the position to another device

Pagination moves on a relayout because its cfi is the first visible character of the page, and a relayout cuts the pages differently. Say the reader turned to a page that starts at the word "Alice", then rotates the phone. The page holding "Alice" now starts a few words earlier, at "said", and pagination's begin.cfi becomes "said", while the reader has not moved. Save that and reopen the book in the first orientation, and it opens on the page holding "said", which is the page before. Every save after a rotation moves the reader back again. The reading position stays "Alice", so the book reopens on the page the reader turned to, at any size.

Save the reading position, and open the book at it:

reader.navigation.readingPosition$.subscribe((readingPosition) => {
localStorage.setItem(
`reading-position-${bookId}`,
JSON.stringify(readingPosition),
)
})
// the next time this book is opened
const saved = localStorage.getItem(`reading-position-${bookId}`)
const readingPosition: ReadingPosition | undefined = saved
? JSON.parse(saved)
: undefined
const reader = createReader({
manifest,
target: readingPosition
? { type: "cfi", value: readingPosition.cfi }
: undefined,
})
reader.mount(document.getElementById("reader")!)

The navigation page says exactly when the reading position changes, and why every value is the one to save, whatever its status.

The reading position carries its own percentageEstimateOfBook, the progress to save for a library screen: it moves with the position it describes, where pagination's moves with every relayout and lags each navigation until the new result settles. Anything else you save from pagination has to come from a settled result, as explained in Settlement.

reader.pagination.state$ emits the current result as soon as you subscribe, then every new one. reader.pagination.state is its latest result, read synchronously.

The reader smooths this stream on purpose. A result is built from several sources (layout, navigation, the loaded documents), and while the reader changes state they do not all update at once. Rather than emit each intermediate combination, the stream waits for them to agree, so it can briefly lag behind the reader. A result is only settled once it describes the reader's current position. state lags the same way: it is the stream's latest result, not a fresher one.

A pagination result describes a moment that may still be resolving. While a document loads, or during a navigation or a layout, an edge's cfi can describe the start of an item rather than the page actually being read.

isSettled tells the two apart. A settled result describes the visible pages of the current layout over content that is ready, or failed to load, and it is the only variant whose edges have their cfi typed as present, so TypeScript makes you establish settlement before reading one:

reader.pagination.state$.subscribe((state) => {
if (!state.isSettled) return
saveProgress(bookId, state.percentageEstimateOfBook)
})

Settlement ends as soon as a navigation starts, a layout starts, or a visible item stops being ready, or is unloaded after failing, and returns once a replacement result has resolved over the new state. A layout is not only one you request: the reader lays the spine out again whenever an item finishes loading, fails to load or unloads, so settlement can drop briefly while the book loads around the page being read. A visible item whose load failed does not keep pagination waiting: it settles on it, with the item's start as its edge's cfi, as the error handling page explains. Page metrics such as begin.pageIndexInSpineItem stay available throughout, so navigation controls keep working on estimates while a result is pending.

It describes the current visible position, not the loading state of the whole book: a settled result does not mean every item has been loaded.

The reading position needs no such check: every value it takes is the closest known position of the reader's latest navigation, and says with status whether the reader has found its page yet, or can't, as the navigation page explains.

The two move in a set order. The reading position moves to where a navigation goes before pagination settles there, and once it has moved, every settled result describes the pages the navigation shows, never pages it moved away from. A navigation that keeps the pages shown, such as one to a cfi on the second page of a spread, is no exception: the reading position moves, and pagination settles again on the same pages. An app that keeps pagination's estimate next to the reading position can therefore let the latest value win: the reading position's own percentageEstimateOfBook stands until the settled estimate for the pages holding it replaces it.