Scrivr
Packages

@scrivr/react

React bindings for Scrivr — useScrivrEditor hook, Scrivr component, and useScrivrState selector hook.

@scrivr/react is a thin React adapter over @scrivr/core. It provides a hook to create and manage an Editor instance within the React lifecycle, a mount component, and a state-subscription hook for building toolbars and menus without excessive re-renders.

Installation

pnpm add @scrivr/core @scrivr/react
npm install @scrivr/core @scrivr/react
yarn add @scrivr/core @scrivr/react

Peer dependencies: React 18 or 19.


useScrivrEditor

useScrivrEditor creates an Editor instance on mount and destroys it on unmount. It returns null during the first render (before the effect runs) — <Scrivr /> handles this gracefully.

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

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

Options

OptionTypeDescription
extensionsExtension[]Required. Extensions to load.
pageConfigPageConfigPage dimensions and margins. Defaults to A4, ¾-inch margins.
onCreate({ editor }) => voidCalled once the editor is fully initialized.
onUpdate({ editor }) => voidCalled on every document or selection change.
onSelectionUpdate({ editor }) => voidAlias for onUpdate — fires on every state change.
onFocus({ editor }) => voidCalled when the editor textarea gains focus.
onBlur({ editor }) => voidCalled when the editor textarea loses focus.
onDestroy() => voidCalled when the editor is destroyed.

The optional second argument is a dependency array — if any value in it changes, the editor is torn down and re-initialized:

const editor = useScrivrEditor({ extensions: [StarterKit] }, [roomId]);

Scrivr

<Scrivr /> mounts the ViewManager into a container <div>. It manages the hidden <textarea> input bridge for keyboard and IME events, and cleans up all observers on unmount.

import { Scrivr } from '@scrivr/react';

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

Props

PropTypeDefaultDescription
editorEditor | nullRequired. Editor instance from useScrivrEditor.
gapnumber24Gap in px between pages.
overscannumber500Virtual scroll overscan in px.
classNamestringCSS class on the outer container.
styleCSSPropertiesInline styles on the outer container.

useScrivrState

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

import { useScrivrState } from '@scrivr/react';

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

Use this to build toolbars that stay in sync with cursor position:

export function Toolbar({ editor }: { editor: Editor | null }) {
  const isBold = useScrivrState({
    editor,
    selector: ({ editor }) => editor?.isActive('bold') ?? false,
  });

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

  return (
    <div>
      <button
        onClick={() => editor?.commands.toggleBold()}
        data-active={isBold}
      >
        B
      </button>
      <button
        onClick={() => editor?.commands.toggleItalic()}
        data-active={isItalic}
      >
        I
      </button>
    </div>
  );
}

ScrivrContext and useScrivr

Coming soon. ScrivrContext and useScrivr are not yet available.

In larger applications passing the editor instance down as a prop to every toolbar, menu, and sidebar component gets tedious. ScrivrContext will let you provide the editor once at the top of your tree, and useScrivr will let any child component consume it without props.

Future API (not yet available)
import { ScrivrContext, useScrivr } from '@scrivr/react';

// Provide at the top of your tree
export function EditorApp() {
  const editor = useScrivrEditor({ extensions: [StarterKit] });

  return (
    <ScrivrContext.Provider value={editor}>
      <Toolbar />
      <Scrivr editor={editor} style={{ height: '100vh' }} />
    </ScrivrContext.Provider>
  );
}

// Consume anywhere in the tree — no prop drilling
function Toolbar() {
  const editor = useScrivr();
  return (
    <button onClick={() => editor?.commands.toggleBold()}>Bold</button>
  );
}

@scrivr/react exports five ready-made floating menu components:

ComponentDescription
BubbleMenuAppears above a text selection. Use for inline formatting (bold, italic, link).
FloatingMenuAppears at the start of an empty paragraph. Use for block-type switchers.
LinkPopoverPopover for inserting and editing links.
ImageMenuContext menu for selected images — alignment, float mode, resize.
SlashMenu/-triggered command palette for inserting blocks.
TrackChangesPopoverInline accept/reject popover — appears when the cursor is inside a pending tracked change. Handles single changes and multi-author conflicts.

Full Example

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

export function MyEditor() {
  const editor = useScrivrEditor({
    extensions: [StarterKit],
    pageConfig: {
      pageWidth: 794,
      pageHeight: 1123,
      margins: { top: 72, right: 72, bottom: 72, left: 72 },
    },
  });

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

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

  return (
    <div style={{ height: '100vh', position: 'relative' }}>
      {/* BubbleMenu appears above any text selection */}
      <BubbleMenu editor={editor} className="bubble-menu">
        <button
          onClick={() => editor?.commands.toggleBold()}
          data-active={isBold}
        >
          B
        </button>
        <button
          onClick={() => editor?.commands.toggleItalic()}
          data-active={isItalic}
        >
          I
        </button>
      </BubbleMenu>

      <Scrivr editor={editor} style={{ height: '100%' }} />
    </div>
  );
}

React Integration Guide

On this page