@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/reactnpm install @scrivr/core @scrivr/reactyarn add @scrivr/core @scrivr/reactPeer 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
| Option | Type | Description |
|---|---|---|
extensions | Extension[] | Required. Extensions to load. |
pageConfig | PageConfig | Page dimensions and margins. Defaults to A4, ¾-inch margins. |
onCreate | ({ editor }) => void | Called once the editor is fully initialized. |
onUpdate | ({ editor }) => void | Called on every document or selection change. |
onSelectionUpdate | ({ editor }) => void | Alias for onUpdate — fires on every state change. |
onFocus | ({ editor }) => void | Called when the editor textarea gains focus. |
onBlur | ({ editor }) => void | Called when the editor textarea loses focus. |
onDestroy | () => void | Called 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
| Prop | Type | Default | Description |
|---|---|---|---|
editor | Editor | null | — | Required. Editor instance from useScrivrEditor. |
gap | number | 24 | Gap in px between pages. |
overscan | number | 500 | Virtual scroll overscan in px. |
className | string | — | CSS class on the outer container. |
style | CSSProperties | — | Inline 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.
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>
);
}Menu Components
@scrivr/react exports five ready-made floating menu components:
| Component | Description |
|---|---|
BubbleMenu | Appears above a text selection. Use for inline formatting (bold, italic, link). |
FloatingMenu | Appears at the start of an empty paragraph. Use for block-type switchers. |
LinkPopover | Popover for inserting and editing links. |
ImageMenu | Context menu for selected images — alignment, float mode, resize. |
SlashMenu | /-triggered command palette for inserting blocks. |
TrackChangesPopover | Inline accept/reject popover — appears when the cursor is inside a pending tracked change. Handles single changes and multi-author conflicts. |
Full Example
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>
);
}