Skip to content

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.

TiptapProseKit
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

import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'

const editor = new Editor({
  element: document.querySelector('#editor')!,
  extensions: [StarterKit],
  content: '<p>Hello</p>',
})

editor.commands.toggleBold()

ProseKit

import { defineBasicExtensionfunction defineBasicExtension(): BasicExtension
Define a basic extension that includes some common functionality. You can copy this function and customize it to your needs. It's a combination of the following extension functions: - {@link defineDoc } - {@link defineText } - {@link defineParagraph } - {@link defineHeading } - {@link defineList } - {@link defineBlockquote } - {@link defineImage } - {@link defineHorizontalRule } - {@link defineHardBreak } - {@link defineTable } - {@link defineCodeBlock } - {@link defineItalic } - {@link defineBold } - {@link defineUnderline } - {@link defineStrike } - {@link defineCode } - {@link defineLink } - {@link defineBaseKeymap } - {@link defineBaseCommands } - {@link defineHistory } - {@link defineGapCursor } - {@link defineVirtualSelection } - {@link defineModClickPrevention }
@public
} from 'prosekit/basic'
import { createEditorfunction createEditor<E extends Extension>(options: EditorOptions<E>): Editor<E>
@public
} from 'prosekit/core'
const editorconst editor: Editor<BasicExtension> = createEditorcreateEditor<BasicExtension>(options: EditorOptions<BasicExtension>): Editor<BasicExtension>
@public
({
extensionEditorOptions<BasicExtension>.extension: BasicExtension
The extension to use when creating the editor.
: defineBasicExtensionfunction defineBasicExtension(): BasicExtension
Define a basic extension that includes some common functionality. You can copy this function and customize it to your needs. It's a combination of the following extension functions: - {@link defineDoc } - {@link defineText } - {@link defineParagraph } - {@link defineHeading } - {@link defineList } - {@link defineBlockquote } - {@link defineImage } - {@link defineHorizontalRule } - {@link defineHardBreak } - {@link defineTable } - {@link defineCodeBlock } - {@link defineItalic } - {@link defineBold } - {@link defineUnderline } - {@link defineStrike } - {@link defineCode } - {@link defineLink } - {@link defineBaseKeymap } - {@link defineBaseCommands } - {@link defineHistory } - {@link defineGapCursor } - {@link defineVirtualSelection } - {@link defineModClickPrevention }
@public
(),
defaultContentEditorOptions<E extends Extension>.defaultContent?: string | NodeJSON | Element | undefined
The starting document to use when creating the editor. It can be a ProseMirror node JSON object, an HTML string, or a DOM element instance.
: '<p>Hello</p>',
}) const rootconst root: Element | null = documentvar document: Document
**`window.document`** returns a reference to the document contained in the window. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/document)
.querySelectorParentNode.querySelector<Element>(selectors: string): Element | null (+4 overloads)
Returns the first element that is a descendant of node that matches selectors. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/querySelector)
('#editor')
if (rootconst root: Element | null) editorconst editor: Editor<BasicExtension>.mountEditor<BasicExtension>.mount: (place: HTMLElement | null | undefined) => void | VoidFunction
Mount the editor to the given HTML element. Pass `null` or `undefined` to unmount the editor. When an element is passed, this method returns a function to unmount the editor.
(rootconst root: Element as HTMLElement)
editorconst editor: Editor<BasicExtension>.commands
Editor<BasicExtension>.commands: ToCommandAction<{
    setParagraph: [];
    setHeading: [attrs?: HeadingAttrs | undefined];
    insertHeading: [attrs?: HeadingAttrs | undefined];
    toggleHeading: [attrs?: HeadingAttrs | undefined];
    dedentList: [options?: DedentListOptions];
    indentList: [options?: IndentListOptions];
    moveList: [direction: "up" | "down"];
    splitList: [];
    toggleCollapsed: [options?: ToggleCollapsedOptions];
    ... 58 more ...;
    redo: [];
}>
All {@link CommandAction } s defined by the editor.
.toggleBold
toggleBold: CommandAction
() => boolean
Execute the current command. Return `true` if the command was successfully executed, otherwise `false`.
()
  • There is no chain(). Each command runs as a single transaction. For composite commands, write a function that returns a Command and pass it to editor.exec(...).
  • Hooks are framework-specific. The Tiptap React useEditorState is closest to ProseKit's useEditorDerivedValue.
RemirrorProseKit
class FooExtension extends NodeExtensiondefineNodeSpec({ name, ... })
class FooExtension extends MarkExtensiondefineMarkSpec({ name, ... })
class FooExtension extends PlainExtensiona 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

import 'remirror/styles/all.css'

import { Remirror, useRemirror } from '@remirror/react'
import { BoldExtension, ItalicExtension, UnderlineExtension } from 'remirror/extensions'

function extensions () {
  return [
  new BoldExtension(),
  new ItalicExtension(),
  new UnderlineExtension(),
]
}

export function Editor() {
  const { manager, state } = useRemirror({
    extensions,
    content: '<p>Hello</p>',
    stringHandler: 'html',
  })

  return <Remirror manager={manager} initialContent={state} />
}

ProseKit

'use client'

import 'prosekit/basic/style.css'

import { defineBasicExtension } from 'prosekit/basic'
import { createEditor } from 'prosekit/core'
import { ProseKit } from 'prosekit/react'
import { useMemo } from 'react'

export function Editor() {
  const editor = useMemo(() => {
    return createEditor({
      extension: defineBasicExtension(),
      defaultContent: '<p>Hello</p>',
    })
  }, [])

  return (
    <ProseKit editor={editor}>
      <div ref={editor.mount} />
    </ProseKit>
  )
}
  • No extension classes. Remirror extensions are subclasses of NodeExtension / MarkExtension / PlainExtension, instantiated with new and wired through a manager. ProseKit extensions are plain values returned from define* functions, composed with union(...). Lifecycle methods like createKeymap() and createInputRules() become standalone defineKeymap({...}) and defineInputRule(...) in the same union.
  • No manager / state split. Remirror gives you { manager, state, onChange } and you wire all three into <Remirror>. ProseKit gives you a single editor instance you mount into a DOM node via editor.mount (a callback ref) inside <ProseKit editor={editor}>.
  • No chain().run(). Each editor.commands.foo() call runs as its own transaction. To group several edits into one transaction, write a function that returns a ProseMirror Command and pass it to editor.exec(...).
  • active and enabled are methods on the editor, not React hooks. Read them inline (e.g. editor.commands.toggleBold.canExec() or editor.marks.bold.isActive()) and pull the booleans out through useEditorDerivedValue when you want React to re-render on change.
  • Helpers live on the editor, not in useHelpers(). editor.getDocHTML() and editor.getDocJSON() replace the Remirror helpers. There's no built-in getText(), so read editor.view.state.doc.textContent if you need the plain string.
  • Subscribing to changes uses hooks, not components. Replace <OnChangeJSON onChange={...}> with useDocChange (fires on document edits) or useStateUpdate (fires on every state update, including selection changes).

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-managing Schema, EditorState, and a plugin array.
  • Typed commands. editor.commands.toggleBold() instead of a loose dispatch(toggleMark(...)).
  • Node/mark actions. editor.marks.bold.isActive() and editor.nodes.heading.isActive({ level: 1 }) without writing selection-walking helpers.
  • Per-event handlers. defineKeyDownHandler, definePasteHandler, etc., instead of writing a Plugin for each.
  • A consistent extension model. Every feature (your custom ones included) ships as a union(...) of small parts.

Wrap any Plugin you already have with definePlugin and union it in:

import { definePlugin
function definePlugin(plugin: Plugin | Plugin[] | ((context: {
    schema: Schema;
}) => Plugin | Plugin[])): PlainExtension
Adds a ProseMirror plugin to the editor.
@paramplugin - The ProseMirror plugin to add, or an array of plugins, or a function that returns one or multiple plugins.@public
} from 'prosekit/core'
import { Pluginclass Plugin<PluginState = any>
Plugins bundle functionality that can be added to an editor. They are part of the [editor state](https://prosemirror.net/docs/ref/#state.EditorState) and may influence that state and the view that contains it.
, PluginKeyclass PluginKey<PluginState = any>
A key is used to [tag](https://prosemirror.net/docs/ref/#state.PluginSpec.key) plugins in a way that makes it possible to find them, given an editor state. Assigning a key does mean only one plugin of that type can be active in a state.
} from 'prosekit/pm/state'
const myPluginconst myPlugin: PlainExtension = definePlugin
function definePlugin(plugin: Plugin | Plugin[] | ((context: {
    schema: Schema;
}) => Plugin | Plugin[])): PlainExtension
Adds a ProseMirror plugin to the editor.
@paramplugin - The ProseMirror plugin to add, or an array of plugins, or a function that returns one or multiple plugins.@public
(
new Pluginnew Plugin<number>(spec: PluginSpec<number>): Plugin<number>
Create a plugin.
({
keyPluginSpec<PluginState>.key?: PluginKey<any> | undefined
Can be used to make this a keyed plugin. You can have only one plugin with a given key in a given state, but it is possible to access the plugin's configuration and state through the key, without having access to the plugin instance object.
: new PluginKeynew PluginKey<any>(name?: string): PluginKey<any>
Create a plugin key.
('my-plugin'),
statePluginSpec<number>.state?: StateField<number> | undefined
Allows a plugin to define a [state field](https://prosemirror.net/docs/ref/#state.StateField), an extra slot in the state object in which it can keep its own data.
: {
initStateField<number>.init: (config: EditorStateConfig, instance: EditorState) => number
Initialize the value of the field. `config` will be the object passed to [`EditorState.create`](https://prosemirror.net/docs/ref/#state.EditorState^create). Note that `instance` is a half-initialized state instance, and will not have values for plugin fields initialized after this one.
: () => 0,
applyStateField<number>.apply: (tr: Transaction, value: number, oldState: EditorState, newState: EditorState) => number
Apply the given transaction to this state field, producing a new field value. Note that the `newState` argument is again a partially constructed state does not yet contain the state from plugins coming after this one.
: (__: Transaction, valuevalue: number) => valuevalue: number + 1,
}, }), )

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:

import { defineNodeSpec
function defineNodeSpec<Node extends string, Attrs extends AnyAttrs = Attrs>(options: NodeSpecOptions<Node, Attrs>): Extension<{
    Nodes: { [K in Node]: Attrs; };
}>
Defines a node type into the editor schema.
@public@example```ts const extension = defineNodeSpec({ name: 'fancyParagraph', content: 'inline*', group: 'block', parseDOM: [{ tag: 'p.fancy' }], toDOM() { return ['p', { 'class': 'fancy' }, 0] }, }) ```
} from 'prosekit/core'
const fancyParagraph
const fancyParagraph: Extension<{
    Nodes: {
        fancyParagraph: Attrs;
    };
}>
= defineNodeSpec
defineNodeSpec<"fancyParagraph", Attrs>(options: NodeSpecOptions<"fancyParagraph", Attrs>): Extension<{
    Nodes: {
        fancyParagraph: Attrs;
    };
}>
Defines a node type into the editor schema.
@public@example```ts const extension = defineNodeSpec({ name: 'fancyParagraph', content: 'inline*', group: 'block', parseDOM: [{ tag: 'p.fancy' }], toDOM() { return ['p', { 'class': 'fancy' }, 0] }, }) ```
({
nameNodeSpecOptions<"fancyParagraph", Attrs>.name: "fancyParagraph"
The name of the node type.
: 'fancyParagraph',
contentNodeSpec.content?: string | undefined
The content expression for this node, as described in the [schema guide](https://prosemirror.net/docs/guide/#schema.content_expressions). When not given, the node does not allow any content.
: 'inline*',
groupNodeSpec.group?: string | undefined
The group or space-separated groups to which this node belongs, which can be referred to in the content expressions for the schema.
: 'block',
parseDOMNodeSpec.parseDOM?: readonly TagParseRule[] | undefined
Associates DOM parser information with this node, which can be used by [`DOMParser.fromSchema`](https://prosemirror.net/docs/ref/#model.DOMParser^fromSchema) to automatically derive a parser. The `node` field in the rules is implied (the name of this node will be filled in automatically). If you supply your own parser, you do not need to also specify parsing rules in your schema.
: [{ tagTagParseRule.tag: string
A CSS selector describing the kind of DOM elements to match.
: 'p.fancy' }],
toDOMNodeSpec.toDOM?: ((node: Node) => DOMOutputSpec) | undefined
Defines the default way a node of this type should be serialized to DOM/HTML (as used by [`DOMSerializer.fromSchema`](https://prosemirror.net/docs/ref/#model.DOMSerializer^fromSchema)). Should return a DOM node or an [array structure](https://prosemirror.net/docs/ref/#model.DOMOutputSpec) that describes one, with an optional number zero (“hole”) in it to indicate where the node's content should be inserted. For text nodes, the default is to create a text DOM node. Though it is possible to create a serializer where text is rendered differently, this is not supported inside the editor, so you shouldn't override that in your text node spec.
: () => ['p', { classclass: string: 'fancy' }, 0],
})

A ProseMirror Command is just a (state, dispatch?, view?) => boolean. You can pass yours directly to editor.exec(...):

import { defineBasicExtensionfunction defineBasicExtension(): BasicExtension
Define a basic extension that includes some common functionality. You can copy this function and customize it to your needs. It's a combination of the following extension functions: - {@link defineDoc } - {@link defineText } - {@link defineParagraph } - {@link defineHeading } - {@link defineList } - {@link defineBlockquote } - {@link defineImage } - {@link defineHorizontalRule } - {@link defineHardBreak } - {@link defineTable } - {@link defineCodeBlock } - {@link defineItalic } - {@link defineBold } - {@link defineUnderline } - {@link defineStrike } - {@link defineCode } - {@link defineLink } - {@link defineBaseKeymap } - {@link defineBaseCommands } - {@link defineHistory } - {@link defineGapCursor } - {@link defineVirtualSelection } - {@link defineModClickPrevention }
@public
} from 'prosekit/basic'
import { createEditorfunction createEditor<E extends Extension>(options: EditorOptions<E>): Editor<E>
@public
} from 'prosekit/core'
import type { Commandtype Command = (state: EditorState, dispatch?: (tr: Transaction) => void, view?: EditorView) => boolean
Commands are functions that take a state and a an optional transaction dispatch function and... - determine whether they apply to this state - if not, return false - if `dispatch` was passed, perform their effect, possibly by passing a transaction to `dispatch` - return true In some cases, the editor view is passed as a third argument.
} from 'prosekit/pm/state'
declare const myExistingCommandconst myExistingCommand: Command: Commandtype Command = (state: EditorState, dispatch?: (tr: Transaction) => void, view?: EditorView) => boolean
Commands are functions that take a state and a an optional transaction dispatch function and... - determine whether they apply to this state - if not, return false - if `dispatch` was passed, perform their effect, possibly by passing a transaction to `dispatch` - return true In some cases, the editor view is passed as a third argument.
const editorconst editor: Editor<BasicExtension> = createEditorcreateEditor<BasicExtension>(options: EditorOptions<BasicExtension>): Editor<BasicExtension>
@public
({ extensionEditorOptions<BasicExtension>.extension: BasicExtension
The extension to use when creating the editor.
: defineBasicExtensionfunction defineBasicExtension(): BasicExtension
Define a basic extension that includes some common functionality. You can copy this function and customize it to your needs. It's a combination of the following extension functions: - {@link defineDoc } - {@link defineText } - {@link defineParagraph } - {@link defineHeading } - {@link defineList } - {@link defineBlockquote } - {@link defineImage } - {@link defineHorizontalRule } - {@link defineHardBreak } - {@link defineTable } - {@link defineCodeBlock } - {@link defineItalic } - {@link defineBold } - {@link defineUnderline } - {@link defineStrike } - {@link defineCode } - {@link defineLink } - {@link defineBaseKeymap } - {@link defineBaseCommands } - {@link defineHistory } - {@link defineGapCursor } - {@link defineVirtualSelection } - {@link defineModClickPrevention }
@public
() })
editorconst editor: Editor<BasicExtension>.execEditor<BasicExtension>.exec: (command: Command) => boolean
Execute the given command. Return `true` if the command was successfully executed, otherwise `false`.
(myExistingCommandconst myExistingCommand: Command)

To expose it as a named action (editor.commands.myThing()), wrap it in a defineCommands:

import { defineCommands
function defineCommands<T extends Record<string, CommandCreator> = Record<string, CommandCreator>>(commands: T): Extension<{
    Commands: { [K in keyof T]: Parameters<T[K]>; };
}>
} from 'prosekit/core'
import type { Commandtype Command = (state: EditorState, dispatch?: (tr: Transaction) => void, view?: EditorView) => boolean
Commands are functions that take a state and a an optional transaction dispatch function and... - determine whether they apply to this state - if not, return false - if `dispatch` was passed, perform their effect, possibly by passing a transaction to `dispatch` - return true In some cases, the editor view is passed as a third argument.
} from 'prosekit/pm/state'
declare const myExistingCommandconst myExistingCommand: Command: Commandtype Command = (state: EditorState, dispatch?: (tr: Transaction) => void, view?: EditorView) => boolean
Commands are functions that take a state and a an optional transaction dispatch function and... - determine whether they apply to this state - if not, return false - if `dispatch` was passed, perform their effect, possibly by passing a transaction to `dispatch` - return true In some cases, the editor view is passed as a third argument.
defineCommands
defineCommands<{
    myThing: () => Command;
}>(commands: {
    myThing: () => Command;
}): Extension<{
    Commands: {
        myThing: [];
    };
}>
({
myThingmyThing: () => Command: () => myExistingCommandconst myExistingCommand: Command, })

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.

  • createEditor doesn't take a state directly. Pass your initial doc as defaultContent (JSON, HTML, or a DOM element).
  • editor.view throws until you call editor.mount(...). Use editor.mounted to check before reading view-dependent state, and don't read view during construction.
  • Plugins added via editor.use(...) persist until you call the returned dispose function. Don't use(...) inside a command. Wire it from your component / setup code instead.