prosemirror-state
Immutable, plugin-extensible editor state for ProseMirror, with transactional document, selection, and stored-mark updates.
Repository Health
Technical Analysis
prosemirror-state is the core state module of ProseMirror, the toolkit for building rich, well-behaved WYSIWYG editors on top of contentEditable. It defines the EditorState object — an immutable snapshot of the current document, selection, and stored marks — and the Transaction type used to move from one state to the next. Rather than mutating an editor in place, every change (typing, formatting, drag-and-drop, collaborative merges) is expressed as a transaction applied to produce a brand-new state, which makes undo history, time travel, and collaborative editing tractable to implement correctly.
On top of that base, the module provides ProseMirror’s plugin system: a Plugin can declare its own StateField that rides along inside EditorState, hook into transaction filtering and appending, and contribute view-level props. This is the layer that libraries like prosemirror-example-setup, prosemirror-collab, and countless editor products (Notion-style block editors, comment/annotation systems, collaborative docs) build their behavior on, without needing to touch prosemirror-view or prosemirror-model directly.
What You Get
- EditorState — an immutable state object holding doc, selection, and storedMarks, created via EditorState.create and advanced via state.apply(transaction)
- Transaction — a Transform subclass adding selection tracking, stored-mark management, scroll-into-view flags, and arbitrary setMeta/getMeta metadata for coordinating with plugins
- Selection, TextSelection, NodeSelection, and AllSelection — a small class hierarchy covering cursor/range text selections, whole-node selections, and select-all, with SelectionRange and SelectionBookmark for multi-range and serializable selection state
- Plugin and PluginKey — a plugin abstraction that can add a StateField (custom persisted state), filterTransaction and appendTransaction hooks, and EditorProps, keyed so state can be looked up without holding the plugin instance
- JSON serialization — EditorState.toJSON/fromJSON round-trips document, selection, and opted-in plugin state for persistence or transmission
Common Use Cases
- Building a custom rich-text or block-based editor (comment threads, wiki pages, CMS content fields) where you need full control over document state and undo behavior
- Implementing collaborative editing by combining Transaction/appendTransaction hooks with prosemirror-collab to merge concurrent changes deterministically
- Writing editor plugins that need their own persisted, transaction-driven state (e.g. tracked changes, live cursors, inline suggestions) via a custom StateField
- Serializing and restoring editor sessions (drafts, versioned documents) with EditorState.toJSON/fromJSON
Under The Hood
Architecture — The module centers on three cooperating pieces: EditorState (src/state.ts), Transaction (src/transaction.ts), and Plugin/PluginKey (src/plugin.ts). EditorState doesn’t store its fields as a fixed set of properties; instead, a Configuration object built from the active plugin list assembles a fields array — the four built-in FieldDescs (doc, selection, storedMarks, scrollToSelection) plus one FieldDesc per plugin that declares a state spec — and applyInner walks that array calling each field’s apply(tr, oldValue, oldState, newState) to produce the next state. EditorState.applyTransaction layers a second loop on top of apply: after the root transaction is applied, every plugin with an appendTransaction hook gets a chance to inspect the new transactions and append its own (used for auto-corrections, collaborative rebasing, etc.), tracked per-plugin via a seen array so a plugin never re-sees transactions it already processed. Selection is a small class hierarchy (Selection abstract base in src/selection.ts, with TextSelection, NodeSelection, AllSelection subclasses) that all implement map() so selections survive document changes.
Tech Stack — Pure TypeScript (100% of the codebase per GitHub’s language breakdown), targeting ES modules with a CJS build (dist/index.js / dist/index.cjs) generated via the @prosemirror/buildhelper dev dependency and pm-buildhelper. Runtime dependencies are exactly the other ProseMirror core modules — prosemirror-model (document/schema), prosemirror-transform (the Transform/Step base that Transaction extends), and prosemirror-view (only for the EditorView/EditorProps types used in plugin specs) — with no third-party runtime dependencies at all.
Code Quality — Mocha-driven tests live in test/test-state.ts and test/test-selection.ts (352 lines combined) using the ist assertion library and prosemirror-test-builder for constructing test documents; coverage includes transaction application, plugin state fields (init/apply/toJSON/fromJSON), filterTransaction/appendTransaction interplay, and selection mapping across document changes. The source itself is compact (about 1,100 lines across five files) with extensive /// doc comments used to generate the published API reference, consistent use of TypeScript generics for typed plugin state (Plugin<PluginState>, PluginKey<PluginState>), and internal invariants enforced with explicit RangeError throws (e.g. rejecting a setSelection call with a selection pointing at a stale document).
API Design — The public surface is deliberately small and composes tightly with prosemirror-transform: state.tr returns a fresh Transaction, transaction methods return this for chaining (tr.setSelection(...).scrollIntoView()), and plugin authors interact with a single StateField<T> interface (init/apply/optional toJSON/fromJSON) rather than a sprawling plugin API. The tradeoff is a steep initial learning curve — understanding the transaction/appendTransaction/filterTransaction interplay and the immutable-state model takes real study — but once internalized, the API is highly predictable and the same few primitives (Transaction, StateField, PluginKey) cover nearly every plugin use case in the ecosystem.
Used by 16 apps in this directory
Anarlog
Note Taking · AI Assistants · Productivity
Anarlog is an open-source, local-first AI meeting notetaker that records, transcribes, and summarizes meetings entirely on your device — no cloud lock-in, no mandatory account, and every note saved as a plain markdown file you own forever.
cmux
Developer Tools · AI Development
A native, Ghostty-based macOS terminal with vertical tabs, agent-aware notifications, and a scriptable browser built for running many parallel AI coding agent sessions instead of juggling tmux panes.
Discourse
Community
Open-source community platform with dynamic trust levels, real-time chat, and a powerful plugin architecture for self-hosted forums.
Dittofeed
Marketing · Automation
Open-source omni-channel customer engagement platform for automating transactional and marketing messages via email, SMS, WhatsApp, Slack, and mobile push.
Epicenter
Knowledge Management · Note Taking · Developer Tools
A local-first monorepo led by Whispering, an open-source speech-to-text app, built on an MIT toolkit that turns your data into plain Markdown and SQLite files you own instead of a database you rent.
Joplin
Note Taking
The privacy-first, open-source note-taking app with end-to-end encrypted sync, AI assistance, and a powerful plugin ecosystem across every platform.
Keila
Marketing
Self-hosted newsletter platform with a visual block editor, Liquid personalization, and flexible email delivery — a privacy-respecting Mailchimp alternative you run on your own infrastructure.
LearnHouse
Learning Management · CMS
Open-source LMS with AI tutoring, real-time collaboration boards, live code execution, and built-in course monetization — self-hosted in minutes.
lila (Lichess)
Community
The forever-free, ad-free, open-source chess server powering millions of real-time games, AI analysis, puzzles, and tournaments worldwide.