PagedEditorContent renders EditorContent on measured pages. Plugin
elements, marks, handlers, shortcuts and slots render as usual. The paged view
measures pages, renders page chrome, places blocks and can leave offscreen
pages out of the DOM. The editor value stays the source of content, and page
geometry is derived view state.
import { createPretextPageLayoutEngine } from 'platejs/pagination';
import {
PagedEditorContent,
usePageLayout,
} from 'platejs/pagination/react';
import { EditorRoot, useCreateEditor } from 'platejs/react';
import { useMemo, useRef } from 'react';
export function DocumentEditor() {
const editor = useCreateEditor({ initialValue: value });
const editableRef = useRef<HTMLDivElement>(null);
const engine = useMemo(() => createPretextPageLayoutEngine(), []);
const layout = usePageLayout(editableRef);
return (
<EditorRoot editor={editor}>
<output>{layout?.pages.length ?? 0} pages</output>
<PagedEditorContent
ref={editableRef}
engine={engine}
page={{ margins: 72, preset: 'letter' }}
pageView={{ gap: 24, mode: 'single' }}
renderPage={({ attributes, page }) => (
<div {...attributes} aria-label={`Page ${page.index + 1}`} />
)}
/>
</EditorRoot>
);
}import { createPretextPageLayoutEngine } from 'platejs/pagination';
import {
PagedEditorContent,
usePageLayout,
} from 'platejs/pagination/react';
import { EditorRoot, useCreateEditor } from 'platejs/react';
import { useMemo, useRef } from 'react';
export function DocumentEditor() {
const editor = useCreateEditor({ initialValue: value });
const editableRef = useRef<HTMLDivElement>(null);
const engine = useMemo(() => createPretextPageLayoutEngine(), []);
const layout = usePageLayout(editableRef);
renderPage receives page attributes and geometry. It renders page chrome;
the paged view places the editable content on the same canvas. Spread mode
changes page arrangement without changing the document.
usePageLayout reads the layout published by the exact editable element in
its ref. This keeps sibling paged editors independent and returns null before
the surface publishes its first valid layout.
Text blocks use inline flow by default. Describe how images, embeds, tables, and other non-inline structures fragment:
import type { NodeFragmentationProvider } from 'platejs/pagination';
const fragmentation: NodeFragmentationProvider = ({ content, element }) => {
if (element.type === 'image') {
return { type: 'atomic', size: { height: 320, width: 480 } };
}
if (element.type === 'table') {
return {
type: 'atomic',
size: { height: element.children.length * 44, width: content.width },
};
}
if (element.type === 'codeBlock') {
return { type: 'text', keepTogether: true };
}
};
<PagedEditorContent
page={{ margins: 72, preset: 'letter' }}
fragmentation={fragmentation}
/>;import type { NodeFragmentationProvider } from 'platejs/pagination';
const fragmentation: NodeFragmentationProvider = ({ content, element }) => {
if (element.type === 'image') {
return { type: 'atomic', size: { height: 320, width: 480 } };
}
if (element.type === 'table') {
return {
type: 'atomic',
size: { height: element.children.length * 44, width: content.width },
};
}
if (element.type === 'codeBlock'
atomic places one indivisible box. A table renders as one atomic block: a
table shorter than a page moves to the next page whole when it does not fit.
direct-children places each immediate child while the document tree stays
intact. text can request soft keep-together behavior; content taller than a
page still fragments.
The app owns these sizes. Without a plan, a void block such as an image measures as one empty line, and a top-level block whose children are other blocks makes measurement fail; the editor stays editable and renders without pages until the document measures again.
Use usePageLayoutFragments() inside an element renderer to read the current
element's mounted page placements. The hook derives the element path from the
renderer context.
createPretextPageLayoutEngine() measures rich inline text with browser font
metrics. The app owns typography: return the fonts, line heights, and block
spacing your element styles render, or the measured lines will not match the
painted text. Call measurePages with an explicit engine for headless
snapshots. Use createEstimatedPageLayoutEngine() where browser text
measurement is unavailable.
Keep fragmentation and typography stable across renders, for example with
useMemo. A new function or object remeasures the document.
A server cannot measure text, so a server-rendered page shows the editor without pages, and pages appear once the browser measures after hydration.
Each measured line renders at its measured position inside its block. Element
padding and margins do not move that text, so put the vertical space they add
into typography block spacing instead. Horizontal space has no typography
equivalent, so an indented block, such as a list item, paints its text at the
content edge, under its list marker.
measurePages(editor, options) reads one coherent editor snapshot and returns an immutable PageLayoutSnapshot. It neither subscribes nor publishes. A custom PageLayoutEngine receives detached immutable input and must conserve source ranges, fragment order, paths, and declared non-text sizes.
Different font files and browser profiles can produce different line breaks. Derived pages are therefore a view result, not stored page-break authority. Products that require identical exported pages need one controlled measurement environment for that export.
Pass a named root to PagedEditorContent or measurePages to measure and render that root. Omit root for the primary document. Both reject "main", an internal root spelling.
page accepts a PageSettings value or a defineStateField field, so the app keeps shared page settings in editor state without storing derived geometry. Persist that field with persist: { ...pageSettingsCodec, version: 1 }, which stores validated finite settings; the built-in presets are "a4" and "letter".
PagedEditorContent mounts every page by default. Set virtualize only after
the complete paged DOM exceeds the product's tested interaction or memory
budget. Virtualization keeps selected and promoted content mounted, but native
browser find, accessibility traversal, DOM printing, and third-party DOM
integrations see mounted content only. Use model-based export for the complete
document.