Plate
PlateEditorsTemplates
GitHub16kGitHub
DiscordDiscord
  • Feature Kits
  • Plugin
    • Plugin Methods
    • Plugin Shortcuts
    • Plugin Context
    • Plugin Components
    • Plugin Rules
    • Editing Behavior
    • Plugin Input Rules
  • Editor
    • Editor Methods
    • Controlled Value
  • Authored Changes
  • Performance
  • Static Rendering
  • HTML
  • Markdown
  • Form
  • TypeScript
  • Debugging
  • Unit Testing
  • Browser
  • Troubleshooting
  • Locations
  • Transactions
  • Serializing
  • Roots
  • Document Meta
  • Clipboard and Paste
  • Decorations, annotations, and widgets
  • Schema
  • History
  • Pagination
  • Annotations
  • DOM Coverage
  • External Text Views
  • Virtualized Rendering

Pagination

PreviousNext

Render a Plate editor as measured pages with explicit fragmentation.

PagedEditable owns page measurement, page chrome, document placement, and optional page-level DOM omission. The editor value remains the source of content; page geometry is derived view state.

Installation

pnpm add platejs @chenglou/pretext
pnpm add platejs @chenglou/pretext

Render a paged editor

document-editor.tsx




























HistoryAnnotations

On This Page

InstallationRender a paged editorFragment non-text blocksMeasurementDOM coverage
Build your editor
Production-ready AI template and reusable components.
Get all-access
import { createPretextPageLayoutEngine } from 'platejs/pagination';
import {
PagedEditable,
usePageLayout,
} from 'platejs/pagination/react';
import { Plate, 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 (
<Plate editor={editor}>
<output>{layout?.pages.length ?? 0} pages</output>
<PagedEditable
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}`} />
)}
/>
</Plate>
);
}
document-editor.tsx
import { createPretextPageLayoutEngine } from 'platejs/pagination';
import {
  PagedEditable,
  usePageLayout,
} from 'platejs/pagination/react';
import { Plate, 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 (
    <Plate editor={editor}>
      <output>{layout?.pages.length ?? 0} pages</output>
      <PagedEditable
        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}`} />
        )}
      />
    </Plate>
  );
}

renderPage receives page attributes and geometry. It renders page chrome; PagedEditable 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.

Fragment non-text blocks

Text blocks use inline flow by default. Images, embeds, tables, and other non-inline structures must describe how they fragment:

<PagedEditable
  page={{ margins: 72, preset: 'letter' }}
  fragmentation={({ element }) => {
    if (element.type === 'image') {
      return { type: 'atomic', size: { height: 320, width: 480 } };
    }
 
    if (element.type === 'table') {
      return {
        type: 'direct-children',
        sizes: element.children.map(() => ({ height: 36, width: 624 })),
      };
    }
 
    if (element.type === 'code_block') {
      return { type: 'text', keepTogether: true };
    }
  }}
/>
<PagedEditable
  page={{ margins: 72, preset: 'letter' }}
  fragmentation={({ element }) => {
    if (element.type === 'image') {
      return { type: 'atomic', size: { height: 320, width: 480 } };
    }
 
    if (element.type === 'table') {
      return {
        type: 'direct-children',
        sizes: element.children.map(() => ({ height: 36, width: 624 })),
      };
    }
 
    if (element.type ===



atomic places one indivisible box. direct-children lets pagination place each immediate child while the document tree stays intact. text can request soft keep-together behavior; content taller than a page still fragments.

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.

Measurement

createPretextPageLayoutEngine() measures rich inline text with browser font metrics. Supply matching typography callbacks when rendered fonts, line heights, or block spacing differ from the defaults. Call measurePages with an explicit engine for headless snapshots. createEstimatedPageLayoutEngine() is useful where browser text measurement is unavailable.

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.

DOM coverage

PagedEditable 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.

'code_block'
) {
return { type: 'text', keepTogether: true };
}
}}
/>