Transform EPUB into a series of HTML files for reading directly in the browser, with built-in Media Overlays support too.
By default, the converter splits a content document containing several chapters into one page per
chapter at its table of contents entries. Use --no-split-chapters to preserve the source content
documents as pages. Heading-based page titles are also on by default; use --no-heading-titles to
turn them off. Multiple top-level entries always identify chapters; nested entries are used as
chapter boundaries only in long content documents (at least 50 KiB), so ordinary chapter files
are not split into their subsections.
See an example fileset):
After conversion, the original EPUB is rewritten as:
Media Overlays conversion adds the following:
September 2026
See Reading features and Keyboard shortcuts below.
These demos were created using this conversion script.
Test books:
source-epubs/notes-test; the narration is synthetic (macOS say).audio-map.json retain their per-page timeline.* expands siblings, typing finds the next matching entry, and Enter follows the selected link. Find a section narrows the tree to entries whose title contains the typed text, in any language; Down arrow moves into the results, Enter follows the first one, and Escape clears the field.Search works within one book. Results link to the exact passage, including narration cue targets
when a book has synchronized audio. The reader loads search-data.json and builds its in-memory
index only after the first search.
Narrated results use the existing ids required by their timing cues. Other results use Web
Annotation-style TextQuoteSelector anchors: the passage’s exact text plus nearby prefix and
suffix context. The reader resolves the quotation after loading the page, so search does not add
generated ids to publication content.
Search uses Intl.Segmenter to split text into words according to its language, including scripts
such as Chinese, Japanese, Thai, Lao, Khmer and Burmese that do not put spaces between every word.
When a browser lacks Intl.Segmenter, the fallback splits on Unicode whitespace and punctuation
and adds overlapping two-character pieces for Han, Hiragana, Katakana and Hangul text.
Matching ignores case and Unicode compatibility differences such as full-width forms and ligatures.
Combining marks remain significant because they are part of letters in Indic scripts, Vietnamese
and many other languages. Two explicit language rules ignore optional Arabic harakat and tatweel,
and Hebrew niqqud; new exceptions belong in the small rule table in search-text.js, with the
language-specific reason documented there.
The final query word can match a prefix. One-character typo tolerance applies only to words of at
least five characters written entirely in Latin, Cyrillic or Greek letters, where an edit is less
likely to change a vowel sign or tone mark. Search does not currently stem words or ignore accents
in Latin-script languages. Accurate lang markup in the publication improves word segmentation,
case folding, display direction and screen-reader pronunciation.
| Shortcut | Where it works | What it does |
|---|---|---|
Escape |
Anywhere, only while the “Skip past” notification is showing | Skip past the current table, list, figure, sidebar, footnote, endnote, or note during narration playback |
[ |
Anywhere outside a text field or open popover | Previous phrase, continuing onto the previous page from the first phrase of a page |
] |
Anywhere outside a text field or open popover | Next phrase, continuing onto the next page from the last phrase of a page |
\ |
Anywhere outside a text field or open popover | Play / pause |
Alt+Shift+P |
Anywhere outside a text field or open popover | Go to narration position: moves focus, and a screen reader, to the phrase narration is on, e.g. to read on from there after pausing |
Alt+Shift+R |
Anywhere outside a text field or open popover | Play from here: starts narration at the phrase that has focus (a screen reader’s cursor brings focus with it) or at selected text |
Alt+Shift+L |
Anywhere outside a text field or open popover | Announce the listening time left in the current chapter and book |
Alt+Shift+Q |
Anywhere outside a text field or open popover | Announce the current print page |
| Unassigned | While narration is on a phrase containing a note reference | Go to the note in this phrase, or open its list of notes |
| Unassigned | After following Go to note | Back to text at the exact reference and narration position |
Shift+[ / Shift+] |
Anywhere outside a text field or open popover | Previous / next page |
Shift+\ |
Anywhere outside a text field or open popover | Caption mode on or off |
Alt+Shift+C |
Anywhere outside a text field or open popover | Open the table of contents |
Alt+Shift+F |
Anywhere outside a text field or open popover | Open search |
Alt+Shift+G |
Anywhere outside a text field or open popover | Open Go to page |
Alt+Shift+S |
Anywhere outside a text field or open popover | Open Settings |
Alt+Shift+O |
Anywhere outside a text field or open popover | Open About this publication |
Alt+Shift+, / Alt+Shift+. |
Anywhere outside a text field or open popover | Narration slower / faster |
Alt+Shift+- / Alt+Shift+= |
Anywhere outside a text field or open popover | Smaller / larger text |
↑ ↓ ← → |
Anywhere outside a form control or open popover, only while Caption mode is on | Same as [ / ] (on a page without narration, steps through its text) |
The narration keys are unmodified single keys. An earlier version used Ctrl+Alt combinations, and screen reader users reported clashes with their assistive technology’s commands (e.g. JAWS/NVDA table-navigation keys). The narration slider uses the usual slider keys (arrows, Page Up/Page Down, Home/End); on the full-book timeline they move by the amounts described under Reading features.
Open Settings, then choose Change keyboard shortcuts to open the shortcut editor. Choose Change next to an action and press its new key, with any modifiers. Escape cancels a change. If another action already uses the key, the editor shows and announces a warning before asking you to confirm the move. Remove clears an action’s shortcut; Restore default restores one action, and Reset to defaults restores the table above. Choices are shared across books on the same website and remembered when you return.
On a Mac, Alt is Option. Escape closes an open panel. The panel, narration position, speed and text size keys use Alt+Shift because NVDA, JAWS, Narrator and VoiceOver don’t use it with letters or punctuation, and Chrome, Edge and Safari don’t use these letters with it; Shift+, and Shift+. would have clashed with NVDA’s and JAWS’s browse-mode commands. Escape stays reserved for Skip past; Tab (including Shift+Tab), Enter and Space are reserved for using controls. The editor warns when a key is also a screen reader browse-mode command (an unmodified letter or some punctuation, or Shift+, and Shift+.). Caption mode’s arrow keys remain fixed.
More about [, ] and \:
P. On other layouts the same physical keys work even though they type different characters (on a German keyboard, Ü, + and #), so they never need AltGr.Shift, [ and ] move by page and \ turns caption mode on or off. With any other modifier they do nothing unless you assign that exact combination.The hardware media keys are separate from these: play/pause plays and pauses, and previous/next track move to the previous or next page.
Every page is a complete HTML page. Without JavaScript, it has a plain header with a Contents button (the table of contents opens without JavaScript, as a native popover) and links to the previous and next pages, and narrated pages have the browser’s own audio player at the bottom. The reading interface’s toolbars only appear when its script runs.
npm run convert -- <input> <output> template --audio-cache [dir] keeps each page’s merged narration in dir (or a folder in the system’s temp folder) and reuses it on the next conversion when the page’s audio clips haven’t changed. Merging clips is most of a conversion’s time. Off by default.npm run check:a11y (in convert/) runs axe-core on every page of the demos in headless Chrome and fails on WCAG 2.2 A or AA violations; each book’s first page is also checked with each panel open, in the light and dark themes, and without JavaScript (with the table of contents open too). Pass a folder to check other converted books, --pages N to check fewer pages, --report file.json for the details. It needs Chrome or Chromium (CHROME_PATH if it isn’t found). A GitHub Actions workflow runs it on every push that changes the demos or the converter.