Skip to content

Custom extensions

Most editor features in ProseKit are themselves extensions. When the official ones don’t fit, you write your own and union them in.

Goal Reach for
Bind a key defineKeymap
Run a command defineCommands
Listen to events defineKeyDownHandler, definePasteHandler, …
Add a node or mark defineNodeSpec, defineMarkSpec
Add an attribute to an existing node defineNodeAttr
Drop in a raw ProseMirror plugin definePlugin
Render a node with a framework component Custom node views

Most of those are one-call extensions. You only need a “full” custom extension when you’re combining several of them.

The official extensions follow a consistent shape. Here’s what defineBold looks like, simplified:

import { defineCommands
function defineCommands<T extends Record<string, CommandCreator> = Record<string, CommandCreator>>(commands: T): Extension<{
    Commands: { [K in keyof T]: Parameters<T[K]>; };
}>
, defineKeymapfunction defineKeymap(keymap: Keymap): PlainExtension
Adds a set of keybindings to the editor. Please read the [documentation](https://prosemirror.net/docs/ref/#keymap) for more details.
, defineMarkSpec
function defineMarkSpec<Mark extends string, Attrs extends AnyAttrs = Attrs>(options: MarkSpecOptions<Mark, Attrs>): Extension<{
    Marks: { [K in Mark]: Attrs; };
}>
Defines a mark type into the editor schema.
@example```ts const extension = defineMarkSpec({ name: 'bold', parseDOM: [ { tag: 'strong' }, { tag: 'b' }, ], toDOM() { return ['strong', 0] }, }) ```
, unionfunction union<const E extends readonly Extension[]>(...exts: E): Union<E> (+1 overload)
Merges multiple extensions into one. You can pass multiple extensions as arguments or a single array containing multiple extensions.
@throwsIf no extensions are provided.@example```ts function defineFancyNodes() { return union( defineFancyParagraph(), defineFancyHeading(), ) } ```@example```ts function defineFancyNodes() { return union([ defineFancyParagraph(), defineFancyHeading(), ]) } ```
, type Extensioninterface Extension<T extends ExtensionTyping<any, any, any> = ExtensionTyping<any, any, any>>, type Union
type Union<E extends readonly Extension[]> = Extension<{
    Nodes: ExtractNodes<E[number]>;
    Marks: ExtractMarks<E[number]>;
    Commands: ExtractCommands<E[number]>;
}>
@internal
} from 'prosekit/core'
import { toggleMarkfunction toggleMark({ type, attrs, removeWhenPresent, enterInlineAtoms, }: ToggleMarkOptions): Command
Returns a command that toggles the given mark with the given attributes.
@paramoptions
} from 'prosekit/core'
const myBoldSpecconst myBoldSpec: Extension<ExtensionTyping<any, any, any>>: Extensioninterface Extension<T extends ExtensionTyping<any, any, any> = ExtensionTyping<any, any, any>> = defineMarkSpec
defineMarkSpec<"bold", Attrs>(options: MarkSpecOptions<"bold", Attrs>): Extension<{
    Marks: {
        bold: Attrs;
    };
}>
Defines a mark type into the editor schema.
@example```ts const extension = defineMarkSpec({ name: 'bold', parseDOM: [ { tag: 'strong' }, { tag: 'b' }, ], toDOM() { return ['strong', 0] }, }) ```
({
nameMarkSpecOptions<"bold", Attrs>.name: "bold"
The name of the mark type.
: 'bold',
parseDOMMarkSpec.parseDOM?: readonly ParseRule[] | undefined
Associates DOM parser information with this mark (see the corresponding [node spec field](https://prosemirror.net/docs/ref/#model.NodeSpec.parseDOM)). The `mark` field in the rules is implied.
: [{ tagTagParseRule.tag: string
A CSS selector describing the kind of DOM elements to match.
: 'strong' }, { tagTagParseRule.tag: string
A CSS selector describing the kind of DOM elements to match.
: 'b' }],
toDOMMarkSpec.toDOM?: ((mark: Mark, inline: boolean) => DOMOutputSpec) | undefined
Defines the default way marks of this type should be serialized to DOM/HTML. When the resulting spec contains a hole, that is where the marked content is placed. Otherwise, it is appended to the top node.
: () => ['strong', 0],
}) const myBoldCommands
const myBoldCommands: Extension<{
    Commands: {
        toggleBold: [];
    };
}>
= defineCommands
defineCommands<{
    toggleBold: () => Command;
}>(commands: {
    toggleBold: () => Command;
}): Extension<{
    Commands: {
        toggleBold: [];
    };
}>
({
toggleBoldtoggleBold: () => Command: () => toggleMarkfunction toggleMark({ type, attrs, removeWhenPresent, enterInlineAtoms, }: ToggleMarkOptions): Command
Returns a command that toggles the given mark with the given attributes.
@paramoptions
({ typeToggleMarkOptions.type: string | MarkType
The mark type to toggle.
: 'bold' }),
}) const myBoldKeymapconst myBoldKeymap: PlainExtension = defineKeymapfunction defineKeymap(keymap: Keymap): PlainExtension
Adds a set of keybindings to the editor. Please read the [documentation](https://prosemirror.net/docs/ref/#keymap) for more details.
({
'Mod-b': toggleMarkfunction toggleMark({ type, attrs, removeWhenPresent, enterInlineAtoms, }: ToggleMarkOptions): Command
Returns a command that toggles the given mark with the given attributes.
@paramoptions
({ typeToggleMarkOptions.type: string | MarkType
The mark type to toggle.
: 'bold' }),
}) export function defineMyBold
function defineMyBold(): Union<readonly [Extension<ExtensionTyping<any, any, any>>, Extension<{
    Commands: {
        toggleBold: [];
    };
}>, PlainExtension]>
() {
return union
union<readonly [Extension<ExtensionTyping<any, any, any>>, Extension<{
    Commands: {
        toggleBold: [];
    };
}>, PlainExtension]>(exts_0: Extension<ExtensionTyping<any, any, any>>, exts_1: Extension<{
    Commands: {
        toggleBold: [];
    };
}>, exts_2: PlainExtension): Union<readonly [Extension<ExtensionTyping<any, any, any>>, Extension<{
    Commands: {
        toggleBold: [];
    };
}>, PlainExtension]> (+1 overload)
Merges multiple extensions into one. You can pass multiple extensions as arguments or a single array containing multiple extensions.
@throwsIf no extensions are provided.@example```ts function defineFancyNodes() { return union( defineFancyParagraph(), defineFancyHeading(), ) } ```@example```ts function defineFancyNodes() { return union([ defineFancyParagraph(), defineFancyHeading(), ]) } ```
(
myBoldSpecconst myBoldSpec: Extension<ExtensionTyping<any, any, any>>, myBoldCommands
const myBoldCommands: Extension<{
    Commands: {
        toggleBold: [];
    };
}>
,
myBoldKeymapconst myBoldKeymap: PlainExtension, ) }

Three building blocks (spec, commands, keymap) joined by union. Same pattern works for any feature: define each piece in isolation, then compose. See Concepts → Extensions for the full story.

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 }
} from 'prosekit/basic'
import { createEditorfunction createEditor<E extends Extension>(options: EditorOptions<E>): Editor<E>, defineKeymapfunction defineKeymap(keymap: Keymap): PlainExtension
Adds a set of keybindings to the editor. Please read the [documentation](https://prosemirror.net/docs/ref/#keymap) for more details.
, unionfunction union<const E extends readonly Extension[]>(...exts: E): Union<E> (+1 overload)
Merges multiple extensions into one. You can pass multiple extensions as arguments or a single array containing multiple extensions.
@throwsIf no extensions are provided.@example```ts function defineFancyNodes() { return union( defineFancyParagraph(), defineFancyHeading(), ) } ```@example```ts function defineFancyNodes() { return union([ defineFancyParagraph(), defineFancyHeading(), ]) } ```
} from 'prosekit/core'
const myKeymapconst myKeymap: PlainExtension = defineKeymapfunction defineKeymap(keymap: Keymap): PlainExtension
Adds a set of keybindings to the editor. Please read the [documentation](https://prosemirror.net/docs/ref/#keymap) for more details.
({
'Mod-Shift-x': () => { consolevar console: Console.logConsole.log(...data: any[]): void
The **`console.log()`** static method outputs a message to the console. [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)
('custom shortcut')
return true }, }) const editorconst editor: Editor<Union<readonly [BasicExtension, PlainExtension]>> = createEditorcreateEditor<Union<readonly [BasicExtension, PlainExtension]>>(options: EditorOptions<Union<readonly [BasicExtension, PlainExtension]>>): Editor<Union<readonly [BasicExtension, PlainExtension]>>({ extensionEditorOptions<Union<readonly [BasicExtension, PlainExtension]>>.extension: Union<readonly [BasicExtension, PlainExtension]>
The extension to use when creating the editor.
: unionunion<readonly [BasicExtension, PlainExtension]>(exts_0: BasicExtension, exts_1: PlainExtension): Union<readonly [BasicExtension, PlainExtension]> (+1 overload)
Merges multiple extensions into one. You can pass multiple extensions as arguments or a single array containing multiple extensions.
@throwsIf no extensions are provided.@example```ts function defineFancyNodes() { return union( defineFancyParagraph(), defineFancyHeading(), ) } ```@example```ts function defineFancyNodes() { return union([ defineFancyParagraph(), defineFancyHeading(), ]) } ```
(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 }
(), myKeymapconst myKeymap: PlainExtension),
})

Or, after the editor exists, hot-add it with editor.use(extension) (see Concepts → The Editor).

Ready to share your extension on npm? The prosekit-extension-template repository is a complete, working example. It packages a small YouTube-embed extension and wires up everything you need to ship it and release new versions: a TypeScript build, browser-based tests, and an automated pipeline that publishes to npm from Conventional Commits via release-please and npm trusted publishing (OIDC).

Clone it, rename the package in package.json, and follow its README to cut your first release.