Responsive & mobile
A paginated A4 page fit to a 390px phone is drawn at about 40%: body text ends up around 4px tall. Folio solves this the way Word's Mobile/Web layout and Google Docs with Print layout off do: on small screens the document is reflowed, laid out again by the same engine on a virtual page as wide as the screen, at a readable size. Editing works exactly as in print layout, because it is just another layout of the same document.
import { createLayoutSwitch, FolioView } from '@nextgensoftwares/folio-react';
// One switch per editor (or per app). It owns the layout function.
const layouts = createLayoutSwitch({
options: (doc) => ({ measurer, theme, renderers, headerFooter, hints: format(doc, rules) }),
wrap: composed.wrapLayout, // plugins (TOC page numbers, code highlighting)
mode: 'auto', // 'print' | 'reflow' | 'auto'
initialWidth: window.innerWidth, // a phone's first layout is already reflowed
});
const editor = new FolioEditor({ schema, doc, measurer, layout: layouts.layout });
<FolioView editor={editor} layouts={layouts} mobileBar={{ fonts, defaultFontSize: 11 }} />The host's options(doc) returns its usual layout options withoutcache/previous: the switch keeps one cache and one previous chain per view (print, and the two most recent reflow widths), so every view stays incremental and switching back to print is instant (same document → the same layout object).
Layout modes
| Mode | What it shows |
|---|---|
print | Real pages (the default without a switch). |
reflow | The document rewrapped to the container width at zoom 1. |
auto | Reflow when the container is narrower than breakpoint (720px), or when fitting the page to the width would zoom below minZoom (0.75); print otherwise. In page/spread display the whole page (both pages of a spread) must fit at minZoom. |
resolveLayoutMode(mode, width, pageWidth, options?) is the pure rule; layouts.setMode(mode) changes it at run time (subscribers re-render), and <FolioView layoutMode> overrides the switch's mode for one view.
The Print layout toggle: printLayoutItem(layouts) is a toolbar/menu UIItem (checked in print layout), printLayoutSlot(layouts) puts a switch in the reader bar, and PrintLayoutToggle is the component. Toggling leaves auto for an explicit choice, as Docs does.
What reflow changes
reflowTheme(theme, { width }) (in @nextgensoftwares/folio-layout, pure) derives the virtual page:
- Width = the container minus the view's side padding; no margins, header or footer bands.
- Readable text: every font size is scaled so the body is at least 16px (
minBodySize, at most ×1.5, never smaller than the print size). ExplicitfontSizemarks keep their size. - Narrow screens (text under 480px): list and quote indents, code padding and table cell padding shrink.
- Tables always fit: columns share the width (
colwidths scale down) and long words break inside cells; they never scroll the page sideways. - Images keep their
%width of the (now narrower) column; pixel widths are capped at it. - Floats: square-wrapped objects fall back to top-and-bottom below
floatMinWidth(480px text width). Behind/in-front objects stay.
The view hides the page chrome: no shadows, gaps, rulers, page labels, decorations or page layers (watermarks are print/page features), and there is never a horizontal page scroll.
One continuous column
In scroll display the reflowed document is a single continuous column. The engine lays it out with continuous: true: sheets are fixed-height windows (1600px) cut anywhere on the column, so there are no gaps between sheets. A line or picture straddling a cut belongs to the sheet holding its top and is drawn overflowing onto the next one; backgrounds are split at the cut. The list ends at the last line (no blank tail). Positions, caret, selection, collaborators' carets, find and TOC navigation go through the same position index as print layout; a click on the overflowing part of a straddling line is routed to the sheet that owns it.
Pagination-only features in reflow
| Feature | In the reflow column | In reflow screen pages (e-reader) |
|---|---|---|
Page breaks (pageBreakBefore, "H1 starts a page") | ignored | honoured |
| Keep with next, widows/orphans | ignored | honoured |
| Headers, footers, page numbers | not shown | not shown (the reader bar shows "Screen 37 of 412 · print p. 112") |
| Watermarks / page layers | hidden | hidden |
| TOC block page numbers | computed for the reflow sheets | computed for the screen pages |
Nothing about the document changes: switch back to print and every rule applies again.
E-reader pages (reflow + page/spread display)
Display modes and layout modes are separate axes. Reflow with display="scroll" is the continuous column; with display="page" or "spread" the reflowed text is cut into screen pages like Kindle or Apple Books: a virtual page as wide as the screen (half of it for a spread) and as tall as the viewport, laid out with the engine's own pagination, so cuts fall between lines and widow/orphan/keep rules apply. Navigation is the reader's: swipe, arrow keys, tap zones, the reader bar's slider.
Rotating or resizing keeps the reading position: the view anchors on the document position at the top of the screen, not on a page number, and puts it back in the new layout. Screen page numbers differ from printed ones; screenPageSlot(layouts) adds "Screen 37 of ≈412 · print p. 112" to the reader bar (≈ while the layout is still progressive; the print page needs a print layout from this session, layouts.printPageOf(block)).
Progressive layout (big books)
Switching a 35,000-block book to a new width would re-measure every block (≈2 s). Instead the first reflow is lazy: blocks around the reading position and the caret are measured, all others get an estimated height (drawn as a skeleton, never blank paper). The view then measures the rest in idle slices, visible blocks first, refreshing the layout every ~700ms while keeping the reading position still, and stops estimating when done.
The engine side is plain layout API: layoutDocument(doc, { lazy }), warmLayout(doc, options, indices) (fills the cache with exactly the keys the next layout looks up), estimatedBlocks(layout), countUncached. Only documents with many unmeasured blocks are estimated, so an edit's new block is always measured (the caret never lands in a placeholder).
Measured on the stress book (35,000 blocks, Chromium headless, 390×844):
| First reflowed text on screen after opening | ≈0.9 s (≈0.12–0.2 s of it layout; the rest is generating and parsing the book) |
| Whole book reflowed in idle time | ≈3.5 s after opening, 5 refreshes of 20–50 ms |
| Typing a character (layout / full frame) | 10 ms / 46 ms avg (print on desktop: 7 ms / 39 ms) |
| Enter | 19 ms / 66 ms avg |
Phone editing chrome
In the reflow column on phones (coarse pointer or a compact view), the top area is replaced by <MobileBar>: a formatting bar docked to the bottom, right above the on-screen keyboard (tracked with visualViewport), with undo/redo, marks and lists, plus More opening a sheet with every other toolbar group and plugin item. After typing, the caret is kept above the keyboard and the bar (useKeyboardInset, keyboardInset, scrollForCaret). Pass mobileBar={false} to keep your own top bar, or an object (essentials, fonts, defaultFontSize...) to configure it. Screen pages (page/spread) are a reading mode and keep the host's top bar (reader bar).
Zoom
Zoom applies to print layout (reflow text is already readable, so pinch and Ctrl+wheel are off there).
- Ctrl/⌘+wheel and trackpad pinch zoom around the pointer; touch pinch around the pinch centre; Ctrl/⌘ + = − 0 step or reset to 100% (the browser's page zoom is prevented while the editor has focus).
- While a gesture runs, only the page stack is CSS-scaled; after a 150ms pause the real zoom is committed (no relayout: zoom never changes layout) and the point under the pointer is put back exactly (
zoomAnchor). - 25%–400%. Steps follow Word/Docs (
stepZoom); presets 50–200%, page width, whole page, two pages. <FolioView onZoomChange>reports changes; the host stores the zoom (per document if it likes) and passes it back aszoom. A zoom the user chose is honoured exactly;autoFitonly shrinks the host's initial zoom.<ZoomControl zoom onChange view pageSize onTwoPages>: − slider + and a percentage button with the presets, for a status bar, toolbar or reader bar.useZoomis the headless state behind it.
Editing on a phone = editing on a desktop
Edits are ProseMirror transactions on the same document; only the layout differs. A test applies the same edits in reflow and in print and checks the documents are identical and that the phone's document, switched to print, has the desktop's print layout line for line. The only difference is visual navigation: ArrowUp/Down and Home/End move by the lines on screen, so the same keystrokes can land at a different position in a narrower layout.