Extensions are the building blocks of ProseKit editors. Every piece of functionality - from basic text nodes to complex features like tables - is provided through extensions.
What is an Extension?
An extension is an object that implements the Extension interface (packages/core/src/types/extension.ts):
export interface Extension <
T extends ExtensionTyping < any , any , any > = ExtensionTyping < any , any , any >,
> {
extension : Extension | Extension []
priority ?: Priority
schema : Schema | null
/** @internal */
_type ?: T
}
export interface ExtensionTyping <
N extends NodeTyping = never ,
M extends MarkTyping = never ,
C extends CommandTyping = never ,
> {
Nodes ?: N // Node types and their attributes
Marks ?: M // Mark types and their attributes
Commands ?: C // Command names and their arguments
}
Extensions can provide:
Nodes : Block or inline content types (paragraph, heading, image, etc.)
Marks : Formatting that can be applied to text (bold, italic, link, etc.)
Commands : Functions to modify the document (toggleBold, insertImage, etc.)
Plugins : ProseMirror plugins for behavior and state management
View Props : Configuration for the EditorView
Types of Extensions
ProseKit provides several types of extensions:
1. Node Extensions
Define document structure using defineNodeSpec() (packages/core/src/extensions/node-spec.ts):
import { defineNodeSpec } from '@prosekit/core'
const paragraph = defineNodeSpec ({
name: 'paragraph' ,
content: 'inline*' ,
group: 'block' ,
parseDOM: [{ tag: 'p' }],
toDOM () {
return [ 'p' , 0 ]
},
})
Node Specification Options
export interface NodeSpecOptions <
NodeName extends string = string ,
Attrs extends AnyAttrs = AnyAttrs ,
> extends NodeSpec {
/** The name of the node type */
name : NodeName
/** Whether this is the top-level node (doc node) */
topNode ?: boolean
/** The attributes that nodes of this type get */
attrs ?: {
[ key in keyof Attrs ] : AttrSpec < Attrs [ key ]>
}
// Inherited from ProseMirror NodeSpec:
// content?: string
// marks?: string
// group?: string
// inline?: boolean
// atom?: boolean
// selectable?: boolean
// draggable?: boolean
// code?: boolean
// defining?: boolean
// isolating?: boolean
// parseDOM?: ParseRule[]
// toDOM?: (node: Node) => DOMOutputSpec
// ... and more
}
The content field uses ProseMirror’s content expression syntax. For example:
"inline*" - Zero or more inline nodes
"block+" - One or more block nodes
"text*" - Zero or more text nodes
2. Mark Extensions
Define text formatting using defineMarkSpec() (packages/core/src/extensions/mark-spec.ts):
import { defineMarkSpec } from '@prosekit/core'
const bold = defineMarkSpec ({
name: 'bold' ,
parseDOM: [
{ tag: 'strong' },
{ tag: 'b' },
{ style: 'font-weight=bold' },
],
toDOM () {
return [ 'strong' , 0 ]
},
})
Mark Specification Options
export interface MarkSpecOptions <
MarkName extends string = string ,
Attrs extends AnyAttrs = AnyAttrs ,
> extends MarkSpec {
/** The name of the mark type */
name : MarkName
/** The attributes that marks of this type get */
attrs ?: { [ K in keyof Attrs ] : AttrSpec < Attrs [ K ]> }
// Inherited from ProseMirror MarkSpec:
// inclusive?: boolean
// excludes?: string
// group?: string
// spanning?: boolean
// parseDOM?: ParseRule[]
// toDOM?: (mark: Mark, inline: boolean) => DOMOutputSpec
// ... and more
}
3. Attribute Extensions
Add attributes to existing nodes or marks:
import { defineNodeAttr } from '@prosekit/core'
const paragraphId = defineNodeAttr ({
type: 'paragraph' ,
attr: 'id' ,
default: null ,
parseDOM : ( node ) => node . getAttribute ( 'id' ),
toDOM : ( value ) => value ? [ 'id' , value ] : null ,
})
Attribute extensions must be defined after the node/mark they extend. The base node/mark must exist in the schema first.
4. Plugin Extensions
Add behavior using ProseMirror plugins (packages/core/src/extensions/plugin.ts):
import { definePlugin } from '@prosekit/core'
import { Plugin } from '@prosekit/pm/state'
const myPlugin = definePlugin (
new Plugin ({
state: {
init () { return 0 },
apply ( tr , value ) { return value + 1 },
},
})
)
// Or with a function for schema access
const schemaPlugin = definePlugin (({ schema }) => {
return new Plugin ({
// Use schema here
})
})
5. Command Extensions
Define commands using facets (packages/core/src/facets/command.ts):
import { defineCommand } from '@prosekit/core'
import { toggleMark } from '@prosekit/core'
const toggleBoldCommand = defineCommand ({
name: 'toggleBold' ,
command : () => toggleMark ({ type: 'bold' }),
})
Commands are functions that return ProseMirror Command functions:
import type { Command } from '@prosekit/pm/state'
export type CommandCreator < Args extends any [] = any []> = (
... arg : Args
) => Command
Creating Extensions
ProseKit provides several ways to create extensions:
Using Built-in Helpers
import {
defineNodeSpec ,
defineMarkSpec ,
definePlugin ,
union ,
} from '@prosekit/core'
const myExtension = union (
defineNodeSpec ({ name: 'paragraph' , content: 'inline*' , group: 'block' }),
defineMarkSpec ({ name: 'bold' }),
definePlugin ( /* ... */ ),
)
Using Facet Payloads
For advanced use cases, use defineFacetPayload() directly:
import { defineFacetPayload , pluginFacet } from '@prosekit/core'
const extension = defineFacetPayload ( pluginFacet , [ myPlugin ])
Composing Extensions
Use union() to combine multiple extensions (packages/core/src/editor/union.ts):
import { union } from '@prosekit/core'
function defineBasicNodes () {
return union (
defineDoc (),
defineText (),
defineParagraph (),
defineHeading (),
)
}
function defineBasicMarks () {
return union (
defineBold (),
defineItalic (),
defineCode (),
)
}
const extension = union (
defineBasicNodes (),
defineBasicMarks (),
)
union() can accept extensions as separate arguments or as a single array. Both forms are equivalent and fully typed.
Extension Priority
Extensions have a priority system (0-4, default 2) that controls precedence:
const extension = defineNodeSpec ({
name: 'paragraph' ,
// ...
})
extension . priority = 3 // Higher priority
Priority affects:
Node/Mark Order : Higher priority specs appear first in the schema
Plugin Order : Higher priority plugins run first
Command Resolution : Higher priority commands override lower ones
Merge Behavior : Higher priority values override in conflicts
0 - Lowest : Base extensions that can be overridden
1 - Low : Optional features
2 - Default : Standard extensions
3 - High : Important features that should take precedence
4 - Highest : Critical extensions that must not be overridden
How Extensions Work
Extensions work through the facet system:
1. Extension to Facet Tree
Each extension creates a facet tree (packages/core/src/facets/facet-extension.ts):
export class FacetExtensionImpl < Input , Output > extends BaseExtension {
readonly facet : Facet < Input , Output >
readonly payloads : Input []
createTree ( priority : Priority ) : FacetNode {
const pri = this . priority ?? priority
// Create inputs at the specified priority
const inputs : Tuple5 < Input [] | null > = [ null , null , null , null , null ]
inputs [ pri ] = [ ... this . payloads ]
// Create facet node
let node : FacetNode = new FacetNode ( this . facet , inputs )
// Build tree up to root
while ( node . facet . parent ) {
const children = new Map ([[ node . facet . index , node ]])
node = new FacetNode ( node . facet . parent , undefined , children )
}
return node
}
}
2. Facet Tree Merging
When extensions are combined with union(), their facet trees are merged (packages/core/src/facets/facet-node.ts:70-81):
export function unionFacetNode < I , O >(
a : FacetNode < I , O >,
b : FacetNode < I , O >,
) : FacetNode < I , O > {
return new FacetNode (
a . facet ,
zip5 ( a . inputs , b . inputs , unionInput ), // Merge inputs
unionChildren ( a . children , b . children ), // Merge child nodes
a . reducers , // Reuse reducers
)
}
3. Output Generation
Facet nodes generate output by:
Collecting inputs from all priorities
Collecting child outputs
Applying the reducer function
private calcOutput (): Tuple5 < O | null > {
const inputs: Tuple5 < I [] | null > = [ null , null , null , null , null ]
const output : Tuple5 < O | null > = [ null , null , null , null , null ]
// Collect direct inputs
for ( let pri = 0 ; pri < 5 ; pri ++ ) {
const input = this . inputs [ pri ]
if ( input ) {
inputs [ pri ] = [ ... input ]
}
}
// Collect child outputs as inputs
for ( const child of this . children . values ()) {
const childOutput = child . getOutput ()
for ( let pri = 0 ; pri < 5; pri ++) {
if ( childOutput [ pri ]) {
const input = ( inputs [ pri ] ||= [])
input . push ( childOutput [ pri ] as I )
}
}
}
// Apply reducers
if ( this . facet . singleton ) {
const reducer = this . facet . reducer
const input: I [] = inputs . filter ( isNotNullish ). flat ()
output [ 2 ] = reducer ( input )
} else {
for ( let pri = 0 ; pri < 5; pri ++) {
const input = inputs [ pri ]
if ( input ) {
const reducer = this . facet . reducer
output [ pri ] = reducer ( input )
}
}
}
return output
}
ProseKit’s type system extracts information from extensions:
// Extract node names
type ExtractNodeNames < E extends Extension > = PickStringLiteral <
keyof ExtractNodes < E >
>
// Extract mark names
type ExtractMarkNames < E extends Extension > = PickStringLiteral <
keyof ExtractMarks < E >
>
// Extract command actions
type ExtractCommandActions < E extends Extension > = ToCommandAction <
ExtractCommands < E >
>
This enables full type safety:
const extension = union (
defineDoc (),
defineParagraph (),
defineBold (),
)
type MyExtension = typeof extension
// TypeScript knows the exact types
type Nodes = ExtractNodeNames < MyExtension > // 'doc' | 'paragraph'
type Marks = ExtractMarkNames < MyExtension > // 'bold'
Advanced Extension Patterns
Factory Functions
Create reusable extension factories:
function defineCustomParagraph ( className ?: string ) {
return defineNodeSpec ({
name: 'paragraph' ,
content: 'inline*' ,
group: 'block' ,
attrs: className ? { class: { default: className } } : {},
toDOM ( node ) {
return [ 'p' , { class: node . attrs . class }, 0 ]
},
})
}
const extension = defineCustomParagraph ( 'my-paragraph' )
Conditional Extensions
Include extensions conditionally:
function defineEditor ( options : { spellcheck ?: boolean }) {
const extensions = [
defineDoc (),
defineText (),
defineParagraph (),
]
if ( options . spellcheck ) {
extensions . push ( defineSpellcheck ())
}
return union ( extensions )
}
Extension Overrides
Override built-in extensions with custom versions:
// Base paragraph
const baseParagraph = defineNodeSpec ({
name: 'paragraph' ,
content: 'inline*' ,
group: 'block' ,
})
// Custom paragraph with higher priority
const customParagraph = defineNodeSpec ({
name: 'paragraph' ,
content: 'inline*' ,
group: 'block' ,
attrs: { align: { default: 'left' } },
})
customParagraph . priority = 3
// Custom paragraph will be used
const extension = union ( baseParagraph , customParagraph )
Be careful with overrides - they can break compatibility with other extensions that depend on specific node/mark configurations.
Extension Best Practices
Keep extensions focused : Each extension should do one thing well
Use union() for composition : Group related extensions together
Document attributes : Clearly document any attributes your extensions add
Consider priority : Use default priority (2) unless you have a specific reason
Test compatibility : Ensure your extensions work with common extensions
Provide TypeScript types : Export proper types for your extension’s nodes/marks/commands
Common Extension Patterns
Basic Document Structure
import { union , defineDoc , defineText , defineParagraph } from '@prosekit/core'
const basicExtension = union (
defineDoc (), // Required: top-level document node
defineText (), // Required: text node
defineParagraph (), // At least one block node
)
Rich Text Formatting
import { union , defineBold , defineItalic , defineUnderline , defineCode } from '@prosekit/core'
const formattingExtension = union (
defineBold (),
defineItalic (),
defineUnderline (),
defineCode (),
)
Complete Editor
import { union } from '@prosekit/core'
const extension = union (
// Document structure
defineDoc (),
defineText (),
defineParagraph (),
defineHeading (),
// Formatting
defineBold (),
defineItalic (),
// Features
defineHistory (),
defineKeymap (),
// Plugins
defineBaseKeymap (),
)
Next Steps
Commands Learn about the command system
Schema Understand document schemas