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.

Annotations in an e-book reader let you mark up specific passages — highlights, underlines, sticky notes — and return to them later. In foliate-js, annotations are stored externally as EPUB CFI strings and rendered on top of the page content using an SVG overlay. The library never persists annotations itself; it fires events that give your code the chance to draw whatever you like over a given text range.

How annotations work

When you call view.addAnnotation(), foliate-js resolves the CFI to the correct section, finds the on-screen range, and fires a draw-annotation event. Your event handler calls the provided draw callback with a draw function and any options. The draw function delegates to an Overlayer instance, which maintains an SVG element positioned exactly over the page content. The Overlayer class is the low-level primitive. <foliate-view> creates one overlayer per loaded section automatically and passes it through the renderer’s create-overlayer event.

Adding and removing annotations

// annotation must have a { value } property where value is a CFI string
const annotation = { value: 'epubcfi(/6/4!/4/2/1:0,/1:5,/1:20)' }

await view.addAnnotation(annotation)
await view.deleteAnnotation(annotation)
addAnnotation is async because resolving the CFI may require loading section data. If the annotation’s section is not currently rendered, foliate-js stores it and applies it the next time that section loads. deleteAnnotation(annotation) is a convenience wrapper that calls addAnnotation(annotation, true) internally. It removes the drawn element from the SVG overlay for that section.

Drawing annotations with the draw-annotation event

When a non-search annotation is added, <foliate-view> emits a draw-annotation event. The event detail contains:
  • draw(func, opts) — call this to register the draw function for this annotation
  • annotation — the original annotation object you passed to addAnnotation
  • doc — the section Document object
  • range — the resolved DOM Range
import { Overlayer } from './foliate-js/overlayer.js'

view.addEventListener('draw-annotation', e => {
    const { draw, annotation, range } = e.detail
    // draw a yellow highlight
    draw(Overlayer.highlight, { color: 'yellow' })
})
You call draw once per draw-annotation event. The first argument is the draw function; the second is options passed through to it. foliate-js calls your draw function with (rects, options) where rects is the DOMRectList from range.getClientRects().

Built-in draw functions

Overlayer ships five static draw functions. All accept a DOMRectList as the first argument and an options object as the second.

Overlayer.highlight(rects, options)

Draws filled rectangles behind the text. Opacity and blend mode are controlled via CSS custom properties so they can be changed without redrawing.
draw(Overlayer.highlight, { color: '#facc15' })
Options:
  • color — fill color string (default 'red')
The opacity is set via --overlayer-highlight-opacity (default 0.3) and blend mode via --overlayer-highlight-blend-mode (default normal).

Overlayer.underline(rects, options)

Draws a thin line below each text rect. Supports vertical writing modes.
draw(Overlayer.underline, { color: 'blue', width: 2 })
Options:
  • color — line color (default 'red')
  • width — stroke width in pixels (default 2)
  • writingMode — set to 'vertical-rl' or 'vertical-lr' to place the line on the right edge instead of the bottom

Overlayer.strikethrough(rects, options)

Draws a line through the middle of each text rect. Accepts the same options as underline.

Overlayer.squiggly(rects, options)

Draws a wavy line below each text rect (or to the right in vertical writing). Accepts the same options as underline.

Overlayer.outline(rects, options)

Draws a rounded rectangle border around each text rect. Used internally for search result highlights.
draw(Overlayer.outline, { color: 'orange', width: 3, radius: 4 })
Options:
  • color — stroke color (default 'red')
  • width — stroke width in pixels (default 3)
  • radius — corner radius in pixels (default 3)

Responding to annotation clicks

When the user clicks on a drawn annotation, <foliate-view> fires a show-annotation event:
view.addEventListener('show-annotation', e => {
    const { value, index, range } = e.detail
    // value is the CFI string; show a popover, tooltip, etc.
    console.log('Clicked annotation:', value)
})
value is the same CFI string from the original annotation object. index is the section index, and range is the DOM Range of the annotation. Use these to position a popover or open an edit dialog.
The overlayer does not have event listeners by default. Hit testing is done by comparing click coordinates against the stored client rects of each annotation’s range, not the SVG element itself.

The create-overlay event

<foliate-view> also fires a create-overlay event when an overlayer is first created for a section:
view.addEventListener('create-overlay', e => {
    const { index } = e.detail
    // the overlayer for section `index` is now ready
    // any annotations for this section will be applied automatically
})
You do not need to listen to this event to use annotations — <foliate-view> handles replaying pending annotations automatically. It is primarily useful when you need to know that a specific section’s overlay is initialized.

The Overlayer class interface

If you need lower-level access — for example, when building a custom renderer — you can work with Overlayer directly:
import { Overlayer } from './foliate-js/overlayer.js'

const overlayer = new Overlayer()
// overlayer.element is the <svg> DOM node
someContainer.append(overlayer.element)
overlayer.element
SVGSVGElement
The SVG element. The renderer inserts, sizes, and positions it automatically when you use <foliate-view>.
overlayer.add(key, range, draw, options)
void
Adds an annotation. key can be any value used as a Map key (typically the CFI string). range is a DOM Range or a function that receives the root node and returns a Range. draw is the draw function. If a previous entry with the same key exists, it is removed first.
overlayer.remove(key)
void
Removes the annotation with the given key and its SVG element.
overlayer.redraw()
void
Redraws all annotations by recalculating client rects and calling each draw function again. Called by the renderer after layout changes (page turns, resize).
overlayer.hitTest({ x, y })
[key, range] | []
Returns [key, range] for the topmost annotation at the given viewport coordinates, or an empty array if nothing was hit. Hit testing uses the stored client rects, not the SVG element, so it works even if pointer-events: none is set.

Custom draw functions

You can provide any function that accepts (rects, options) and returns an SVG element:
const drawCircle = (rects, options = {}) => {
    const { color = 'hotpink' } = options
    const ns = 'http://www.w3.org/2000/svg'
    const g = document.createElementNS(ns, 'g')
    for (const { left, top, height, width } of rects) {
        const circle = document.createElementNS(ns, 'ellipse')
        circle.setAttribute('cx', left + width / 2)
        circle.setAttribute('cy', top + height / 2)
        circle.setAttribute('rx', width / 2)
        circle.setAttribute('ry', height / 2)
        circle.setAttribute('fill', 'none')
        circle.setAttribute('stroke', color)
        circle.setAttribute('stroke-width', '2')
        g.append(circle)
    }
    return g
}

view.addEventListener('draw-annotation', e => {
    e.detail.draw(drawCircle, { color: 'hotpink' })
})
Any SVG element or group is valid. The returned element is appended directly to the overlayer’s <svg> root.

Complete example: yellow highlights

import './foliate-js/view.js'
import { Overlayer } from './foliate-js/overlayer.js'

const view = document.createElement('foliate-view')
document.body.append(view)
await view.open('book.epub')

// A simple in-memory annotation store
const annotations = new Map()

view.addEventListener('draw-annotation', e => {
    const { draw, annotation } = e.detail
    const stored = annotations.get(annotation.value) ?? {}
    draw(Overlayer.highlight, { color: stored.color ?? '#fde68a' })
})

view.addEventListener('show-annotation', e => {
    const { value } = e.detail
    console.log('User tapped annotation:', value)
    // open an edit popover, delete confirmation, etc.
})

async function addHighlight(cfi, color = '#fde68a') {
    const annotation = { value: cfi }
    annotations.set(cfi, { color })
    await view.addAnnotation(annotation)
}

async function removeHighlight(cfi) {
    annotations.delete(cfi)
    await view.deleteAnnotation({ value: cfi })
}
To style the opacity of all highlights globally, set --overlayer-highlight-opacity on a parent element or on :root. This avoids the overhead of redrawing every annotation when the user changes a theme setting.

Build docs developers (and LLMs) love