Scrivr
Guides

Viewport & Pagination

Configuring page dimensions, the virtual scroll window, and programmatic scrolling.

Scrivr renders the document as a sequence of fixed-size pages inside a scrollable container. The <Scrivr /> component manages this layout automatically through ViewManager. Only the pages near the visible area are painted at any time — off-screen pages are represented by empty spacer elements.


How pages are displayed

Each page in the DocumentLayout corresponds to a DOM <div> sized to pageConfig.width × pageConfig.height. These wrappers are stacked vertically inside the <Scrivr /> container, separated by the gap value. Scrivr uses an IntersectionObserver to track which pages are visible and only paints those (plus overscan).

┌───── scroll container ──────┐
│  ┌── page-wrapper (div) ──┐ │
│  │  <canvas> content      │ │  ← PageRenderer
│  │  <canvas> overlay      │ │  ← OverlayRenderer
│  └────────────────────────┘ │
│     ← gap (24px default) →  │
│  ┌── page-wrapper (div) ──┐ │
│  │  <canvas> content      │ │
│  │  <canvas> overlay      │ │
│  └────────────────────────┘ │
└─────────────────────────────┘

Controlling page size

Page dimensions and margins are set once via pageConfig in EditorOptions:

const editor = useScrivrEditor({
  extensions: [StarterKit],
  pageConfig: {
    width: 816,   // px — US Letter at 96 dpi
    height: 1056,
    margin: { top: 96, right: 96, bottom: 96, left: 96 }, // 1 inch
  },
});

pageConfig is read-only after construction. See PageConfig for all fields and defaults.


Gap between pages

Control the visual gap between pages with the gap prop on <Scrivr />:

<Scrivr editor={editor} gap={40} />  {/* 40px between pages */}

Default is 24px. The gap is purely visual — it does not affect layout or pagination.


Virtual scrolling and overscan

ViewManager only paints pages that intersect with the scroll viewport ± overscan pixels. Increase overscan if users report blank pages during fast scrolling:

<Scrivr editor={editor} overscan={1000} />

Default is 500px. Higher values keep more pages painted (smoother fast-scroll, more GPU usage).


Observing the current page

editor.cursorPage returns the 1-based page number of the current cursor position:

const page = editor.cursorPage; // e.g. 3

Subscribe to updates to drive a reactive page indicator:

function PageIndicator({ editor }: { editor: Editor }) {
  const page = useScrivrState({
    editor,
    selector: ({ editor }) => editor?.cursorPage ?? 1,
  });

  return <span>Page {page}</span>;
}

Programmatic scrolling

scrollCursorIntoView() scrolls the viewport so the current cursor is visible. It is called automatically after every cursor move — you only need it when moving the cursor programmatically:

editor.moveCursorTo(targetPos);
editor.scrollCursorIntoView();

To scroll to a specific page by pixel offset:

const container = document.querySelector('.my-editor') as HTMLElement;
const scrollParent = container.closest('[style*="overflow"]') ?? container;
scrollParent.scrollTo({
  top: (pageNumber - 1) * (pageHeight + gap),
  behavior: 'smooth',
});

Converting positions to screen coordinates

getViewportRect(from, to) maps a doc-position range to a viewport-relative DOMRect. Use it to position floating UI elements — menus, tooltips, link popovers — relative to text:

const snap = editor.getSelectionSnapshot();
const rect = editor.getViewportRect(snap.from, snap.to);

if (rect) {
  tooltipEl.style.top  = `${rect.bottom + 8}px`;
  tooltipEl.style.left = `${rect.left}px`;
}

Returns null when the positions are not in the character map or no rendering adapter is mounted.

For a selected image, use getNodeViewportRect(docPos) instead — it returns a rect covering the full visual bounds of the node:

const rect = editor.getNodeViewportRect(imageDocPos);
if (rect) {
  toolbar.style.top  = `${rect.bottom + 8}px`;
  toolbar.style.left = `${rect.left}px`;
}

Manual layout control

These methods are rarely needed — the layout engine runs automatically after every transaction.

// Force a synchronous layout pass if stale
editor.ensureLayout();

// Lay out and index a specific page (useful for off-screen coordinate lookups)
editor.ensurePagePopulated(5);

// Trigger a repaint without a doc/selection change (e.g. after awareness data updates)
editor.redraw();

Further reading

On this page