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 { clsx, createEditor, type NodeJSON } from 'prosekit/core'
import { ProseKit } from 'prosekit/react'
import { useMemo } from 'react'

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

import { defineExtension } from './extension'
import { useZoom } from './use-zoom'

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, zoomIn, zoomOut, canZoomIn, canZoomOut } = useZoom()

  return (
    <ProseKit editor={editor}>
      <div className="absolute top-4 left-4 flex items-center justify-center gap-1 border-gray-300 dark:border-gray-600 border p-2 text-sm text-gray-500 select-none print:hidden z-50 bg-gray-50 rounded">
        <button className="size-6 flex justify-center items-center rounded hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-30" onClick={zoomOut} disabled={!canZoomOut}>-</button>
        <span className="w-12 text-center tabular-nums">{zoom}%</span>
        <button className="size-6 flex justify-center items-center rounded hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-30" onClick={zoomIn} disabled={!canZoomIn}>+</button>
      </div>
      <div className="relative w-full flex-1 box-border overflow-y-auto">
        <div
          ref={editor.mount}
          className={clsx('ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500', 'print:transform-none! print:min-h-full! print:p-0! print:m-0!')}
          style={{
            transform: `scale(${zoom / 100})`,
            transformOrigin: 'top',
            minHeight: `${100 / (zoom / 100)}%`,
          }}
        >
        </div>
      </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.