Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/johnfactotum/foliate-js/llms.txt

Use this file to discover all available pages before exploring further.

“Where am I in this book?” has several meaningful answers at once: a byte offset, a chapter name, a page number, a percentage. foliate-js exposes all of these through a single relocate event that fires whenever the visible content changes, plus a pair of helper methods for building chapter-level progress bars and context-aware running heads.

The relocate event

<foliate-view> emits relocate whenever the reader’s position changes — on navigation, page turns, or scroll. Listen to it to update your UI:
view.addEventListener('relocate', e => {
    const {
        cfi,             // string: EPUB CFI of the current position
        fraction,        // number 0–1: progress through the entire book
        index,           // number: index of the current section
        tocItem,         // object | null: nearest TOC entry
        pageItem,        // object | null: nearest page list entry
        location,        // { current, next, total }: location numbers
        time,            // { section, total }: estimated reading time in seconds
        section,         // { current, total }: section counts
        sectionFraction, // (see view.getSectionFractions() below)
    } = e.detail

    console.log(`${Math.round(fraction * 100)}% through the book`)
    console.log('Chapter:', tocItem?.label)
})

Event detail properties

PropertyTypeDescription
cfistringEPUB CFI identifying the current position. Save this to restore the reader’s place.
fractionnumberTotal book progress as a value from 0 to 1, weighted by section byte size.
indexnumberIndex of the currently displayed section in book.sections.
tocItemobject | nullThe deepest TOC entry that precedes the current position. Has .label and .href.
pageItemobject | nullThe deepest page list entry that precedes the current position.
location{ current, next, total }Approximate location numbers derived from byte size (1500 bytes per location by default).
time{ section, total }Estimated remaining reading time in seconds (1600 bytes per second by default).
section{ current, total }The current section index and the total section count.

Saving and restoring position

Save the cfi string from the relocate event and pass it back as lastLocation when you reopen the book:
// Save position
view.addEventListener('relocate', e => {
    localStorage.setItem('lastCFI', e.detail.cfi)
})

// Restore position
await view.open('book.epub')
await view.init({
    lastLocation: localStorage.getItem('lastCFI') ?? null,
    showTextStart: true,  // fall back to the start of the body matter
})
view.init() navigates to lastLocation if it resolves successfully. If lastLocation is null or fails to resolve and showTextStart is true, it navigates to the first landmark marked as bodymatter or the first linear section.
CFIs are tied to the structure of the book file. A CFI from one edition will not correctly identify a position in a different edition of the same text.

Displaying a book-level progress bar

Use fraction from the relocate event directly:
const progressBar = document.getElementById('progress-bar')

view.addEventListener('relocate', e => {
    progressBar.style.width = `${e.detail.fraction * 100}%`
    progressBar.setAttribute('aria-valuenow', Math.round(e.detail.fraction * 100))
})

Displaying a chapter-level progress bar

view.getSectionFractions() returns an array of cumulative fractions, one per section. Use these values to show how far the reader has progressed within the current chapter relative to the entire book.
const chapterBar = document.getElementById('chapter-bar')
let sectionFractions = []

await view.open('book.epub')
sectionFractions = view.getSectionFractions()

view.addEventListener('relocate', e => {
    const { index, fraction } = e.detail
    const sectionStart = sectionFractions[index] ?? 0
    const sectionEnd = sectionFractions[index + 1] ?? 1
    const sectionWidth = sectionEnd - sectionStart

    // Fraction of progress within the current section
    const chapterProgress = sectionWidth > 0
        ? (fraction - sectionStart) / sectionWidth
        : 0

    chapterBar.style.width = `${Math.max(0, Math.min(1, chapterProgress)) * 100}%`
})
The array returned by getSectionFractions() has one extra entry at the end (value 1) so that sectionFractions[index + 1] always gives the section’s end fraction.

Getting TOC and page context for an arbitrary position

view.getProgressOf(index, range) looks up the TOC item and page list item for a given section index and DOM Range. Use this when you need progress information for a position that is not the current view — for example, when generating a preview or building a table of search results.
const { tocItem, pageItem } = view.getProgressOf(index, range)
// tocItem and pageItem have the same shape as in the relocate event
1

Resolve the target to a section index and anchor

Use view.resolveNavigation(cfi) or book.resolveHref(href) to get { index, anchor }.
2

Create a document for the section

Call await book.sections[index].createDocument() to get the section’s Document.
3

Resolve the anchor to a Range

Call anchor(doc) to get a Range within the document.
4

Call getProgressOf

Pass index and the Range to view.getProgressOf(index, range).

Running chapter heads

The paginator renderer exposes .heads and .feet properties — arrays of DOM elements, one per column, that sit in the header and footer regions of each page. Use these to display the current chapter title as a running head.
view.addEventListener('relocate', e => {
    const { tocItem } = e.detail
    const label = tocItem?.label ?? ''

    // renderer is the internal paginator or fixed-layout element
    const { renderer } = view
    if (renderer.heads) {
        for (const head of renderer.heads) {
            head.textContent = label
        }
    }
})
The heads and feet properties are only available in paginated mode (flow="paginated"). In scrolled mode they are absent. Style the head region with the ::part(head) CSS pseudo-element:
foliate-view::part(head) {
    font-size: 0.8em;
    color: graytext;
    padding-bottom: 4px;
    border-bottom: 1px solid graytext;
}
There will be one element in renderer.heads for each visible column. In a two-column layout, update all elements in the array.

Complete example: progress display

import './foliate-js/view.js'

const view = document.createElement('foliate-view')
document.body.append(view)

const chapterLabel = document.getElementById('chapter-label')
const pageLabel = document.getElementById('page-label')
const totalBar = document.getElementById('total-progress')
const locationLabel = document.getElementById('location-label')

await view.open('book.epub')

const sectionFractions = view.getSectionFractions()

await view.init({
    lastLocation: localStorage.getItem('position') ?? null,
    showTextStart: true,
})

view.addEventListener('relocate', e => {
    const { cfi, fraction, index, tocItem, pageItem, location } = e.detail

    // Persist position
    localStorage.setItem('position', cfi)

    // Book-level progress bar
    totalBar.style.width = `${fraction * 100}%`

    // Chapter heading
    chapterLabel.textContent = tocItem?.label ?? ''

    // Page number (if the book has a page list)
    if (pageItem) {
        pageLabel.textContent = `p. ${pageItem.label}`
        pageLabel.hidden = false
    } else {
        pageLabel.hidden = true
    }

    // Location number (Kindle-style)
    locationLabel.textContent = `Loc ${location.current} of ${location.total}`

    // Running head
    if (view.renderer?.heads) {
        for (const head of view.renderer.heads) {
            head.textContent = tocItem?.label ?? ''
        }
    }
})
location.current and location.total are derived from section byte sizes using a fixed bytes-per-location constant (1500 by default). They are meant to be comparable across reading sessions for the same file, not to match any external numbering system.

Build docs developers (and LLMs) love