Real-time collaborative editing for ProseMirror, powered by JSON CRDT and Peritext rich-text CRDT.
- Two-way binding between a ProseMirror
EditorViewand a Peritext CRDT model — local edits are written to the CRDT, remote CRDT changes are patched into ProseMirror with minimal DOM churn. - Remote cursor & selection rendering — optional presence plugin draws colored carets and selection highlights for every connected peer, driven entirely by CSS animations (no JS timers).
- Undo/redo support — works with the standard
prosemirror-historyplugin; remote changes are excluded from the undo stack automatically.
npm install @jsonjoy.com/collaborative-prosemirrorThe package requires ProseMirror core libraries and json-joy to be installed
in your project:
npm install json-joy @jsonjoy.com/collaborative-peritext prosemirror-model prosemirror-state prosemirror-view prosemirror-historyFor collaborative presence (remote cursors), also install:
npm install @jsonjoy.com/collaborative-presenceimport {ModelWithExt, ext} from 'json-joy/lib/json-crdt-extensions';
import {EditorState} from 'prosemirror-state';
import {EditorView} from 'prosemirror-view';
import {schema} from 'prosemirror-schema-basic';
import {ProseMirrorFacade} from '@jsonjoy.com/collaborative-prosemirror';
import {PeritextBinding} from '@jsonjoy.com/collaborative-peritext';
// 1. Create a ProseMirror editor as usual.
const doc = schema.nodes.doc.createAndFill()!;
const view = new EditorView(document.querySelector('#editor')!, {
state: EditorState.create({doc}),
});
// 2. Obtain a PeritextRef — a function that returns the PeritextApi from your
// JSON CRDT model. How you create the model depends on your setup; for
// example:
const json = {"type":"doc","content":[
{"type":"paragraph","content":[{"type":"text","text":"Hello, ProseMirror!"}]},
]};
const model = ModelWithExt.create(ext.peritext.new(''));
const viewRange = FromPm.convert(mySchema.nodeFromJSON(json));
const txt = model.s.toExt().txt;
txt.editor.merge(viewRange);
txt.refresh();
const peritextRef = () => model.s.toExt();
// 3. Create the facade and bind it to the model.
const facade = new ProseMirrorFacade(view, peritextRef);
const unbind = PeritextBinding.bind(peritextRef, facade);
// 4. When done, clean up.
unbind();
facade.dispose();
view.destroy();ProseMirrorFacade accepts an optional third argument — a ProseMirrorFacadeOpts object:
const facade = new ProseMirrorFacade(view, peritextRef, {
history: true, // default: auto-detect
presence: manager, // default: disabled
});Controls whether the prosemirror-history undo/redo plugin is installed.
| Value | Behavior |
|---|---|
undefined |
(default) Install history only when the editor state does not already contain a history plugin. |
true |
Always install the history plugin. |
false |
Never install the history plugin — useful when you handle undo/redo externally. |
Enables the collaborative presence plugin that renders remote cursors and selection highlights.
| Value | Behavior |
|---|---|
undefined / false |
(default) Presence plugin is not installed. |
PresenceManager |
Install with default presence settings. |
PresencePluginOpts |
Install with full control over rendering and timing. See below. |
interface PresencePluginOpts {
manager: PresenceManager;
renderCursor?: CursorRenderer;
renderSelection?: SelectionRenderer;
userFromMeta?: (meta: Meta) => PresenceUser | undefined;
fadeAfterMs?: number; // default: 3000
dimAfterMs?: number; // default: 30000
hideAfterMs?: number; // default: 60000
gcIntervalMs?: number; // default: 5000
}- Local edit to CRDT: ProseMirror transactions are intercepted by the sync
plugin. Simple single-step edits are extracted as
PeritextOperationtuples; complex edits trigger a full document merge via full document diff. - CRDT to ProseMirror: When the CRDT model changes (e.g. a remote patch
arrives),
PeritextBindingcallsfacade.set(). TheToPmNodeconverter builds a new ProseMirror document from the PeritextFragment, reusing cached block nodes.applyPatchthen diffs the old and new documents and emits minimal transaction steps.
This project is funded through NGI Zero Core, a fund established by NLnet with financial support from the European Commission's Next Generation Internet program. Learn more at the NLnet project page.

