Skip to content

Page

The page extension renders the editor content in a paginated layout, similar to traditional word processors like Microsoft Word or Google Docs. Each page has a fixed width, height, and margins. When the content exceeds the available height on a page, it automatically flows to the next page.

import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import 'prosekit/extensions/page/style.css'
import './zoom.css'

import { clsx, createEditor, type NodeJSON } from 'prosekit/core'
import { ProseKit } from 'prosekit/react'
import { useMemo, useState } from 'react'

import { sampleContent } from '../../sample/sample-doc-page'

import { defineExtension } from './extension'
import PaperController from './paper-controller'

interface EditorProps {
  initialContent?: NodeJSON
}

export default function Editor(props: EditorProps) {
  const defaultContent = props.initialContent ?? sampleContent
  const editor = useMemo(() => {
    const extension = defineExtension()
    return createEditor({
      extension,
      defaultContent,
    })
  }, [defaultContent])

  const [zoom, setZoom] = useState(50)

  return (
    <ProseKit editor={editor}>
      <div className="relative w-max min-w-full flex flex-col flex-1 box-border overflow-auto">
        <PaperController zoom={zoom} setZoom={setZoom} />
        <div
          data-editor-zoom="true"
          style={{ '--zoom': zoom / 100 } as React.CSSProperties}
          ref={editor.mount}
          className={clsx('ProseMirror', 'self-center box-border min-h-full m-0 p-10 print:p-0 outline-hidden')}
        />
      </div>
    </ProseKit>
  )
}

Use definePageRendering to enable the paginated layout. You also need to import the page stylesheet.

import 'prosekit/extensions/page/style.css'
import { definePageRenderingfunction definePageRendering(options?: PageRenderingOptions): PageRenderingExtension
@public
} from 'prosekit/extensions/page'
const extensionconst extension: PageRenderingExtension = definePageRenderingfunction definePageRendering(options?: PageRenderingOptions): PageRenderingExtension
@public
()

You can customize the page dimensions and margins:

import { definePageRenderingfunction definePageRendering(options?: PageRenderingOptions): PageRenderingExtension
@public
} from 'prosekit/extensions/page'
const extensionconst extension: PageRenderingExtension = definePageRenderingfunction definePageRendering(options?: PageRenderingOptions): PageRenderingExtension
@public
({
pageWidthPageRenderingOptions.pageWidth?: number | undefined
The width of the page in px.
@default794 (Portrait A4 paper size in 96 DPI)
: 794, // A4 width at 96 DPI (default)
pageHeightPageRenderingOptions.pageHeight?: number | undefined
The height of the page in px.
@default1123 (Portrait A4 paper size in 96 DPI)
: 1123, // A4 height at 96 DPI (default)
marginTopPageRenderingOptions.marginTop?: number | undefined
The top margin of the page in px.
@default70
: 70,
marginRightPageRenderingOptions.marginRight?: number | undefined
The right margin of the page in px.
@default70
: 70,
marginBottomPageRenderingOptions.marginBottom?: number | undefined
The bottom margin of the page in px.
@default70
: 70,
marginLeftPageRenderingOptions.marginLeft?: number | undefined
The left margin of the page in px.
@default70
: 70,
})

Use definePageBreak to add support for manual page breaks. This adds a pageBreak node type to the schema and registers a keyboard shortcut.

import { definePageBreakfunction definePageBreak(): PageBreakExtension
@public
, definePageRenderingfunction definePageRendering(options?: PageRenderingOptions): PageRenderingExtension
@public
} from 'prosekit/extensions/page'
import { unionfunction union<const E extends readonly Extension[]>(...exts: E): Union<E> (+1 overload)
Merges multiple extensions into one. You can pass multiple extensions as arguments or a single array containing multiple extensions.
@throwsIf no extensions are provided.@example```ts function defineFancyNodes() { return union( defineFancyParagraph(), defineFancyHeading(), ) } ```@example```ts function defineFancyNodes() { return union([ defineFancyParagraph(), defineFancyHeading(), ]) } ```@public
} from 'prosekit/core'
const extensionconst extension: Union<readonly [PageRenderingExtension, PageBreakExtension]> = unionunion<readonly [PageRenderingExtension, PageBreakExtension]>(exts_0: PageRenderingExtension, exts_1: PageBreakExtension): Union<readonly [PageRenderingExtension, PageBreakExtension]> (+1 overload)
Merges multiple extensions into one. You can pass multiple extensions as arguments or a single array containing multiple extensions.
@throwsIf no extensions are provided.@example```ts function defineFancyNodes() { return union( defineFancyParagraph(), defineFancyHeading(), ) } ```@example```ts function defineFancyNodes() { return union([ defineFancyParagraph(), defineFancyHeading(), ]) } ```@public
(
definePageRenderingfunction definePageRendering(options?: PageRenderingOptions): PageRenderingExtension
@public
(),
definePageBreakfunction definePageBreak(): PageBreakExtension
@public
(),
)

Insert a manual page break at the current cursor position. The page break forces content after it onto a new page regardless of remaining space.

editorconst editor: Editor<Union<readonly [PageRenderingExtension, PageBreakExtension]>>.commands
Editor<Union<readonly [PageRenderingExtension, PageBreakExtension]>>.commands: ToCommandAction<{
    insertPageBreak: [];
}>
All {@link CommandAction } s defined by the editor.
.insertPageBreak
insertPageBreak: CommandAction
() => boolean
Execute the current command. Return `true` if the command was successfully executed, otherwise `false`.
()
Non-AppleAppleDescription
CtrlEnter CommandEnter Insert a page break

The page extension supports printing via the browser's print dialog. The print output matches the on-screen page layout — each page in the editor becomes a page in the printed document.

Try it out by opening this link and pressing CtrlP (Windows/Linux) or CommandP (Mac).

  • Block-level pagination only. Page breaks can only occur between top-level block elements (paragraphs, headings, images, etc.). A single block element that is taller than the available page height will overflow rather than split across pages. For example, a very long paragraph or a large table cannot be split at a line break.