Scrivr
Getting Started

Quick Start (React)

Build a minimal working Scrivr document editor in a React application.

This guide walks you through mounting a fully functional Scrivr document editor in a React component.

This guide is for React 18 or 19. Vue and Svelte bindings are coming soon. For framework-agnostic usage see @scrivr/core directly.

You will use the two primitives from @scrivr/react:

  • useScrivrEditor — creates and manages an Editor instance within the React lifecycle.
  • <Scrivr /> — mounts the rendering engine onto a DOM container and handles virtual scrolling.

Minimal Example

MyEditor.tsx
import { useScrivrEditor, Scrivr, StarterKit } from '@scrivr/react';

export function MyEditor() {
  const editor = useScrivrEditor({
    extensions: [StarterKit],
  });

  return (
    <Scrivr
      editor={editor}
      style={{ height: '100vh', background: '#f5f5f5' }}
    />
  );
}

Drop <MyEditor /> anywhere in your React tree and you have a paginated, canvas-rendered document editor.


How It Works

1. useScrivrEditor

useScrivrEditor creates an Editor instance on mount and destroys it on unmount. The returned value is null on the very first render (before the effect runs) — <Scrivr /> handles this gracefully by rendering nothing until the editor is ready.

const editor = useScrivrEditor({
  // Extensions to load. StarterKit includes paragraphs, headings,
  // lists, bold, italic, links, history, and more.
  extensions: [StarterKit],

  // Optional: page dimensions and margins. Defaults to A4, ¾-inch margins.
  pageConfig: {
    pageWidth: 794,   // px at 96 dpi — A4 width
    pageHeight: 1123, // px at 96 dpi — A4 height
    margins: { top: 72, right: 72, bottom: 72, left: 72 }, // ¾-inch margins
  },

  // Lifecycle callbacks
  onUpdate: ({ editor }) => console.log('Document changed'),
  onDestroy: () => console.log('Editor destroyed'),
}, [/* Optional dependency array — re-initializes the editor when values change */]);

2. <Scrivr />

<Scrivr /> mounts the document renderer into a container <div>. It manages keyboard and IME input, virtual scrolling, and all cleanup on unmount. You never interact with it beyond passing the editor instance and layout props.

<Scrivr
  editor={editor}
  gap={24}          // gap in px between pages. Default: 24
  overscan={500}    // px beyond the viewport to keep painted. Default: 500
  className="my-editor"
  style={{ height: '100%' }}
/>

Reading Editor State

Use useScrivrState to subscribe to a specific slice of editor state. The component only re-renders when the selected value changes — not on every keypress.

import { useScrivrEditor, Scrivr, useScrivrState, StarterKit } from '@scrivr/react';

export function EditorWithToolbar() {
  const editor = useScrivrEditor({ extensions: [StarterKit] });

  const isBold = useScrivrState({
    editor,
    selector: ({ editor }) => editor?.isActive('bold') ?? false,
  });

  return (
    <>
      <button
        onClick={() => editor?.commands.toggleBold()}
        style={{ fontWeight: isBold ? 'bold' : 'normal' }}
      >
        Bold
      </button>
      <Scrivr editor={editor} style={{ height: '80vh' }} />
    </>
  );
}

Adding Real-Time Collaboration

Turning your editor into a multiplayer one requires just a few extra lines using @scrivr/plugins. Pass the current user's name and colour so remote cursors can be identified.

MultiplayerEditor.tsx
import { useScrivrEditor, Scrivr, StarterKit } from '@scrivr/react';
import { Collaboration, CollaborationCursor } from '@scrivr/plugins';

interface Props {
  documentId: string;
  user: { name: string; color: string };
}

export function MultiplayerEditor({ documentId, user }: Props) {
  const editor = useScrivrEditor({
    extensions: [
      StarterKit.configure({ history: false }), // Y.UndoManager replaces PM history
      Collaboration.configure({
        url: import.meta.env.VITE_WS_URL,
        name: documentId,
      }),
      CollaborationCursor.configure({ user }),
    ],
  }, [documentId]);

  return <Scrivr editor={editor} style={{ height: '100vh' }} />;
}

See the Collaboration example for the full backend setup.


Next Steps

On this page