Migration Guide
ProseKit wraps ProseMirror with a composable, typed API. If you're coming from Tiptap, Remirror, or a hand-rolled ProseMirror setup, most patterns have a direct equivalent. This page collects the ones you'll hit first.
From Tiptap
Section titled “From Tiptap”| Tiptap | ProseKit |
|---|---|
Extension.create({ ... }) | define* factories + union(...) |
useEditor({ extensions, content }) | createEditor({ extension, defaultContent }) |
editor.chain().toggleBold().run() | editor.commands.toggleBold() |
editor.can().toggleBold() | editor.commands.toggleBold.canExec() |
editor.isActive('bold') | editor.marks.bold.isActive() |
editor.isActive('heading', { level: 1 }) | editor.nodes.heading.isActive({ level: 1 }) |
editor.commands.setContent(html) | editor.setContent(html) |
editor.getHTML() / getJSON() | editor.getDocHTML() / getDocJSON() |
Node.create({ name, ... }) | defineNodeSpec({ name, ... }) |
Mark.create({ name, ... }) | defineMarkSpec({ name, ... }) |
addCommands() | defineCommands({ ... }) |
addKeyboardShortcuts() | defineKeymap({ ... }) |
addInputRules() | defineInputRule(...) (regex-driven) |
Tiptap
ProseKit
Sharp edges
Section titled “Sharp edges”- There is no
chain(). Each command runs as a single transaction. For composite commands, write a function that returns aCommandand pass it toeditor.exec(...). - Hooks are framework-specific. The Tiptap React
useEditorStateis closest to ProseKit'suseEditorDerivedValue.
From Remirror
Section titled “From Remirror”| Remirror | ProseKit |
|---|---|
class FooExtension extends NodeExtension | defineNodeSpec({ name, ... }) |
class FooExtension extends MarkExtension | defineMarkSpec({ name, ... }) |
class FooExtension extends PlainExtension | a define* factory that returns an extension |
new BoldExtension(), new ItalicExtension() | defineBold(), defineItalic() |
[ext1, ext2, ext3] | union(ext1, ext2, ext3) |
useRemirror({ extensions, content }) | createEditor({ extension, defaultContent }) |
<Remirror manager={manager} state={state} onChange={...}> | <ProseKit editor={editor}><div ref={editor.mount} /> |
useCommands().toggleBold() | editor.commands.toggleBold() |
useChainedCommands().toggleBold().toggleItalic().run() | call commands separately, or write a custom Command |
commands.toggleBold.enabled() | editor.commands.toggleBold.canExec() |
useActive().bold() | editor.marks.bold.isActive() |
useActive().heading({ level: 1 }) | editor.nodes.heading.isActive({ level: 1 }) |
useHelpers().getHTML() / getJSON() | editor.getDocHTML() / editor.getDocJSON() |
commands.setContent(html) | editor.setContent(html) |
createKeymap() (extension method) | defineKeymap({ ... }) |
createInputRules() (extension method) | defineInputRule(...) |
OnChangeJSON / useEditorEvent('docChanged') | useDocChange(handler) |
Remirror
ProseKit
Sharp edges
Section titled “Sharp edges”- No extension classes. Remirror extensions are subclasses of
NodeExtension/MarkExtension/PlainExtension, instantiated withnewand wired through amanager. ProseKit extensions are plain values returned fromdefine*functions, composed withunion(...). Lifecycle methods likecreateKeymap()andcreateInputRules()become standalonedefineKeymap({...})anddefineInputRule(...)in the sameunion. - No manager / state split. Remirror gives you
{ manager, state, onChange }and you wire all three into<Remirror>. ProseKit gives you a singleeditorinstance you mount into a DOM node viaeditor.mount(a callback ref) inside<ProseKit editor={editor}>. - No
chain().run(). Eacheditor.commands.foo()call runs as its own transaction. To group several edits into one transaction, write a function that returns a ProseMirrorCommandand pass it toeditor.exec(...). activeandenabledare methods on the editor, not React hooks. Read them inline (e.g.editor.commands.toggleBold.canExec()oreditor.marks.bold.isActive()) and pull the booleans out throughuseEditorDerivedValuewhen you want React to re-render on change.- Helpers live on the editor, not in
useHelpers().editor.getDocHTML()andeditor.getDocJSON()replace the Remirror helpers. There's no built-ingetText(), so readeditor.view.state.doc.textContentif you need the plain string. - Subscribing to changes uses hooks, not components. Replace
<OnChangeJSON onChange={...}>withuseDocChange(fires on document edits) oruseStateUpdate(fires on every state update, including selection changes).
From plain ProseMirror
Section titled “From plain ProseMirror”ProseKit is a thin layer over ProseMirror, so your existing schema, plugins, and commands keep working. Most migrations are about replacing ad-hoc setup code with ProseKit primitives, not rewriting your editor from scratch.
What ProseKit gives you on top of ProseMirror
Section titled “What ProseKit gives you on top of ProseMirror”- Composition.
union(define*())instead of hand-managingSchema,EditorState, and a plugin array. - Typed commands.
editor.commands.toggleBold()instead of a loosedispatch(toggleMark(...)). - Node/mark actions.
editor.marks.bold.isActive()andeditor.nodes.heading.isActive({ level: 1 })without writing selection-walking helpers. - Per-event handlers.
defineKeyDownHandler,definePasteHandler, etc., instead of writing aPluginfor each. - A consistent extension model. Every feature (your custom ones included) ships as a
union(...)of small parts.
Reusing what you have
Section titled “Reusing what you have”Existing plugins
Section titled “Existing plugins”Wrap any Plugin you already have with definePlugin and union it in:
Existing schema specs
Section titled “Existing schema specs”defineNodeSpec and defineMarkSpec accept the exact same options as ProseMirror's NodeSpec / MarkSpec. If you have a Schema factory today, port one type at a time:
Existing commands
Section titled “Existing commands”A ProseMirror Command is just a (state, dispatch?, view?) => boolean. You can pass yours directly to editor.exec(...):
To expose it as a named action (editor.commands.myThing()), wrap it in a defineCommands:
Re-exports of the underlying packages
Section titled “Re-exports of the underlying packages”prosekit/pm/* re-exports the standard ProseMirror packages, so you can drop the direct prosemirror-state / prosemirror-model dependencies if you want a single import root. The re-exports are type-compatible with the originals, and using both side-by-side works.
Sharp edges
Section titled “Sharp edges”createEditordoesn't take astatedirectly. Pass your initial doc asdefaultContent(JSON, HTML, or a DOM element).editor.viewthrows until you calleditor.mount(...). Useeditor.mountedto check before reading view-dependent state, and don't readviewduring construction.- Plugins added via
editor.use(...)persist until you call the returned dispose function. Don'tuse(...)inside a command. Wire it from your component / setup code instead.