Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/prosekit/prosekit/llms.txt

Use this file to discover all available pages before exploring further.

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
1
Priority Levels
2
  • 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:
    1. Collecting inputs from all priorities
    2. Collecting child outputs
    3. 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
    }
    

    Type Extraction

    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

    1. Keep extensions focused: Each extension should do one thing well
    2. Use union() for composition: Group related extensions together
    3. Document attributes: Clearly document any attributes your extensions add
    4. Consider priority: Use default priority (2) unless you have a specific reason
    5. Test compatibility: Ensure your extensions work with common extensions
    6. 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

    Build docs developers (and LLMs) love