Skip to main content
ProseKit is a framework-agnostic rich text editor framework built on top of ProseMirror. It provides a modular, type-safe architecture that makes it easy to build custom editors.

Core Architecture

ProseKit’s architecture is built around three key concepts:
  1. Extensions - Modular building blocks that add functionality
  2. Facets - A hierarchical system for composing extensions
  3. Editor - The central orchestrator that brings everything together

The Facet System

At the heart of ProseKit is a facet system that allows extensions to contribute functionality in a composable way. Each facet represents a specific aspect of editor configuration:
// From packages/core/src/facets/facet.ts
export class Facet<Input, Output> {
  readonly index: number
  readonly parent: Facet<Output, any> | null
  readonly singleton: boolean
  readonly path: number[]
  readonly reducer: FacetReducer<Input, Output>
}
Facets use a reducer pattern - they accept an array of inputs and produce a single output. This allows multiple extensions to contribute to the same configuration.

Facet Tree Structure

Facets are organized in a tree structure with the rootFacet at the top:
rootFacet
├── schemaFacet (singleton)
│   ├── nodeSpecFacet
│   └── markSpecFacet
├── stateFacet (singleton)
│   └── pluginFacet
└── commandFacet (singleton)
Each facet can be:
  • Singleton: Only one output is produced (e.g., schema, state)
  • Non-singleton: Multiple outputs are merged (e.g., plugins)

Extension Base Class

All extensions inherit from BaseExtension (packages/core/src/facets/base-extension.ts):
export abstract class BaseExtension<T extends ExtensionTyping = ExtensionTyping> 
  implements Extension<T> {
  abstract extension: Extension | Extension[]
  priority?: Priority
  _type?: T

  // Creates a facet tree for this extension
  abstract createTree(priority: Priority): FacetNode
  
  // Gets the cached facet tree
  getTree(priority?: Priority): FacetNode
  
  // Finds output from a specific facet
  findFacetOutput<I, O>(facet: Facet<I, O>): Tuple5<O | null> | null
  
  // Gets the schema from this extension
  get schema(): Schema | null
}
The Tuple5 type represents five priority levels (0-4), with 2 being the default priority. This allows extensions to control their precedence.

Priority System

ProseKit uses a 5-level priority system for extensions:
// Priority levels: 0 (lowest) to 4 (highest)
// Default priority: 2
type Priority = 0 | 1 | 2 | 3 | 4
type Tuple5<T> = [T, T, T, T, T]
Extensions with higher priority override those with lower priority when conflicts occur.

Extension Composition

ProseKit provides the union() function to compose multiple extensions:
// From packages/core/src/editor/union.ts
function union<const E extends readonly Extension[]>(...exts: E): Union<E>

// Usage
const extension = union(
  defineDoc(),
  defineText(),
  defineParagraph(),
  defineBold(),
)
The union() function:
  1. Flattens nested extensions into a single array
  2. Creates a UnionExtensionImpl that combines all facet trees
  3. Preserves full type information for TypeScript autocomplete
1
How Union Works
2
  • Collect Extensions
    All extensions are flattened into a single array
  • Build Facet Trees
    Each extension creates its facet tree using createTree()
  • Merge Trees
    Trees are merged using unionFacetNode() which:
    • Combines inputs from all extensions
    • Merges child facet nodes recursively
    • Applies reducers to produce final output
  • Type Inference
    TypeScript extracts all nodes, marks, and commands from the union
  • Data Flow

    Here’s how data flows through ProseKit’s architecture:
    ┌─────────────┐
    │ Extensions  │ Define facet payloads
    └──────┬──────┘
    
    
    ┌─────────────┐
    │ Facet Tree  │ Organize and reduce payloads
    └──────┬──────┘
    
    
    ┌─────────────┐
    │ Root Output │ Schema, State, Commands, View props
    └──────┬──────┘
    
    
    ┌─────────────┐
    │   Editor    │ Create EditorState and EditorView
    └─────────────┘
    

    Root Output Structure

    The facet tree produces a root output (packages/core/src/facets/root.ts):
    export type RootOutput = {
      schema?: Schema | null           // ProseMirror schema
      commands?: CommandCreators       // Command creators
      state?: EditorStateConfig        // State configuration
      view?: Omit<DirectEditorProps, 'state'>  // View props
    }
    

    Framework Agnostic Design

    ProseKit’s core is framework-agnostic:
    • Core Package (packages/core/src/): Pure TypeScript, no UI framework dependencies
    • Framework Integrations: React, Vue, Svelte, Solid, Preact packages provide hooks and components
    • Web Components: Standard web components for framework-free usage
    The editor lifecycle is managed independently of the UI framework:
    const editor = createEditor({ extension })
    
    // Mount to DOM (framework-agnostic)
    editor.mount(element)
    
    // Unmount
    editor.unmount()
    

    Type Safety

    ProseKit leverages TypeScript’s type system extensively:
    // Extension typing extracts available APIs
    export interface ExtensionTyping<
      N extends NodeTyping = never,
      M extends MarkTyping = never,
      C extends CommandTyping = never,
    > {
      Nodes?: N
      Marks?: M
      Commands?: C
    }
    
    // Type extraction utilities
    type ExtractNodes<E extends Extension> = ...
    type ExtractMarks<E extends Extension> = ...
    type ExtractCommands<E extends Extension> = ...
    
    This enables:
    • Autocomplete: IDE shows all available nodes, marks, and commands
    • Type Checking: Invalid operations are caught at compile time
    • IntelliSense: Full documentation in tooltips
    The type system relies on TypeScript’s inference. Avoid explicit type annotations when composing extensions to maintain full type information.

    Design Principles

    1. Composability: Extensions can be freely combined using union()
    2. Type Safety: Full TypeScript support with inference
    3. Framework Agnostic: Core logic separate from UI framework
    4. Extensibility: Easy to add custom extensions
    5. Performance: Facet trees are cached and computed lazily
    6. Immutability: Extensions are immutable; editor state changes create new states

    Next Steps

    Editor

    Learn about the Editor lifecycle and API

    Extensions

    Deep dive into the extension system

    Commands

    Understand the command system

    Schema

    Learn about document schemas